{"id":"CVE-2026-54353","title":"@budibase/backend-core has potential SSRF DNS rebinding bypass in outbound fetch validation","summary":"@budibase/backend-core has potential SSRF DNS rebinding bypass in outbound fetch validation","severity":"high","cvss":8.5,"cwe":["CWE-367","CWE-918"],"vendor":"budibase","product":"@budibase/backend-core","ecosystem":"npm","affected":["@budibase/backend-core < 3.39.9"],"patched":["@budibase/backend-core 3.39.9"],"published":"2026-06-22","updated":"2026-06-22","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-gfq7-5x4g-3xhf","references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-gfq7-5x4g-3xhf"},{"url":"https://github.com/advisories/GHSA-gfq7-5x4g-3xhf"}],"tags":["ghsa","npm"],"epss":0.00213,"epssPercentile":0.11873,"ingestedAt":"2026-06-29T13:24:35.485Z","slug":"CVE-2026-54353","body":"## Overview\n\nSummary\n\nAuthenticated users with automation permissions can bypass Budibase's SSRF blacklist through DNS rebinding.\n\nThe outbound fetch flow validates a hostname against the blacklist before the request is sent, but the actual socket connection later performs a separate DNS lookup through node-fetch. Since the validated IPs are never pinned to the connection, an attacker-controlled hostname can return a public IP during validation and a private/internal IP during the real connection.\n\nThis results in a non-blind SSRF primitive against internal services reachable from the Budibase host, including loopback, RFC1918 ranges, and cloud metadata endpoints.\n\nDetails\n\nThe issue comes from the outbound fetch validation flow resolving DNS twice:\n\nDuring blacklist validation\nAgain during the real socket connection\n\nThe first lookup result is discarded after validation, so the second lookup is free to resolve to a different IP.\n\nThis creates a classic TOCTOU DNS rebinding issue.\n\nAffected flow in:\n\npackages/backend-core/src/utils/outboundFetch.ts\n```\nasync function throwIfUnsafe(url: string): Promise<void> {\n  const parsed = parseUrl(url)\n\n  if (await isBlacklisted(parsed.hostname)) {\n    throw new Error(\"URL is blocked or could not be resolved safely.\")\n  }\n}\n\nfor (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {\n  await throwIfUnsafe(nextUrl)\n\n  const response = await fetchFn(nextUrl, nextRequest)\n\n  // ...\n}\n```\nfetchFn uses plain node-fetch with no custom http.Agent / https.Agent, so the underlying socket performs its own independent dns.lookup after validation completes.\n\nThe same pattern also exists in:\n\npackages/server/src/automations/steps/utils.ts\n```\nawait throwIfBlacklisted(nextUrl)\n\nconst response = await fetch(nextUrl, nextRequest)\n```\nThe blacklist implementation resolves hostnames but only returns a boolean:\n\npackages/backend-core/src/blacklist/blacklist.ts\n```\nasync function lookup(address: string): Promise<string[]> {\n  address = parseAddress(address)\n\n  const addresses = await performLookup(address, { all: true })\n\n  return addresses.map(addr => addr.address)\n}\n\nexport async function isBlacklisted(address: string): Promise<boolean> {\n  // ...\n\n  if (!net.isIP(address)) {\n    try {\n      ips = await lookup(address)\n    } catch (e) {\n      /* ... */\n    }\n  } else {\n    ips = [address]\n  }\n\n  return ips.some(ip => blackList!.check(ip, getIpVersion(ip)))\n}\n```\nThe resolved IPs are discarded, so callers cannot pin the later socket connection to the validated addresses.\n\nAn attacker controlling authoritative DNS for a hostname can therefore return:\n\na public IP during validation\na private/internal IP during the actual connection\n\nAnything routing through these helpers inherits the issue, including:\n\noutgoing webhook\nSlack\nDiscord\nMake\nZapier\nn8n\nAI extract\nobject-store fetches\n\nSeveral of these steps return upstream response content directly into automation output, which makes the SSRF non-blind.\n\nPoC\n\nTested locally against a self-hosted build from master.\nNo Budibase-operated infrastructure was touched.\n\nRun Budibase locally.\n\nStart a harmless local HTTP listener:\n\npython3 -m http.server 8080 --bind 127.0.0.1\n\nUse a rebinding hostname such as:\n\n7f000001.cb007264.rbndr.us\n\nwhich rotates between:\n\n127.0.0.1\n203.0.113.100\n\nSteps to reproduce:\n\nLog into Budibase with automation permissions.\nCreate an automation using the Outgoing Webhook step.\nSet the URL to:\nhttp://<rebinding-host>:8080/\nTrigger the automation.\n\nObserved result:\n\nThe blacklist validation resolves the hostname to the public IP and allows the request.\nnode-fetch performs a second DNS lookup during socket creation.\nThe second lookup resolves to 127.0.0.1.\nThe TCP connection lands on the local service.\nThe local server response body appears directly in the automation output.\nImpact\n\nThis produces a non-blind read-SSRF primitive against anything reachable from the Budibase host process, including:\n\nloopback services (127.0.0.1)\nRFC1918 ranges\ninternal Kubernetes/VPC services\ncloud metadata endpoints (169.254.169.254)\n\nOn cloud deployments without IMDSv2 enforcement, this may expose temporary IAM credentials via:\n\n/latest/meta-data/iam/security-credentials/<role>\n\nOn multi-tenant hosted deployments, this may also create potential cross-tenant access paths through shared internal infrastructure.\n\n## Affected packages\n\n- `@budibase/backend-core < 3.39.9`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `@budibase/backend-core 3.39.9`","depth":"twilight","depthScore":47,"depthScoreParts":{"impact":46.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}