Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Advisory and patch confirm no auth (PR:N); attacker writes arbitrary objects in third-party buckets (S:C, I:H), with limited confidentiality and storage-abuse availability impact.
Primary rating from NVD.
CVSS VectorNVD
Lifecycle Timeline
4DescriptionNVD
The application server exposes an unauthenticated endpoint that generates S3 PutObject presigned URLs using credentials stored in a workspace datasource. The route is protected only by the recaptcha middleware and does not require authentication, table permission, datasource permission, or builder access. A public caller who knows a workspace ID and S3 datasource ID can request a signed upload URL for attacker-controlled bucket and key values.
Details
The static route registers the signed upload URL endpoint with only recaptcha before the controller:
packages/server/src/api/routes/static.ts:44-48
44: .post(
45: "/api/attachments/:datasourceId/url",
46: recaptcha,
47: controller.getSignedUploadURL
48: )The controller loads the datasource by datasourceId with enriched secret values:
packages/server/src/api/controllers/static/index.ts:590-598
590:export const getSignedUploadURL = async function (
591: ctx: Ctx<GetSignedUploadUrlRequest, GetSignedUploadUrlResponse>
592:) {
593: // Ensure datasource is valid
594: let datasource
595: try {
596: const { datasourceId } = ctx.params
597: datasource = await sdk.datasources.get(datasourceId, { enriched: true })
598: if (!datasource) {The request body controls bucket and key, and the server signs a PUT URL using the stored datasource credentials:
packages/server/src/api/controllers/static/index.ts:609-629
609: if (datasource?.source === "S3") {
610: const { bucket, key } = ctx.request.body || {}
611: if (!bucket || !key) {
612: ctx.throw(400, "bucket and key values are required")
613: }
614: try {
615: let endpoint = datasource?.config?.endpoint
616: if (endpoint && !utils.urlHasProtocol(endpoint)) {
617: endpoint = `https://${endpoint}`
618: }
619: const s3 = new S3({
620: region: awsRegion,
621: endpoint: endpoint,
622: credentials: {
623: accessKeyId: datasource?.config?.accessKeyId as string,
624: secretAccessKey: datasource?.config?.secretAccessKey as string,
625: },
626: })
627: const params = { Bucket: bucket, Key: key }
628: signedUrl = await getSignedUrl(s3, new PutObjectCommand(params))
629: if (endpoint) {The endpoint returns the signed URL and public URL to the caller:
packages/server/src/api/controllers/static/index.ts:630-639
630: publicUrl = `${endpoint}/${bucket}/${key}`
631: } else {
632: publicUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com/${key}`
633: }
634: } catch (error: any) {
635: ctx.throw(400, error)
636: }
637: }
638:
639: ctx.body = { signedUrl, publicUrl }Because no authorization middleware is applied, the API trusts public input to choose where the stored S3 credentials will write.
PoC
Non-destructive validation approach:
- Create or identify a workspace with an S3 datasource.
- Obtain the production workspace ID and S3 datasource ID.
- Send an unauthenticated request with the workspace ID header and attacker-controlled bucket/key:
POST /api/attachments/<datasourceId>/url HTTP/1.1
x-budibase-app-id: app_<workspace-id>
content-type: application/json
{"bucket":"attacker-controlled-or-permitted-bucket","key":"poc/budibase.txt"}- Observe that the response contains a signed PUT URL.
- Upload harmless content to the returned
signedUrland confirm the object is created using the datasource's stored S3 credentials.
Impact
This allows unauthenticated arbitrary object writes wherever the stored S3 datasource credentials have PutObject access. Depending on the datasource permissions, this can corrupt application data, overwrite public assets, place attacker-controlled objects in trusted buckets, consume storage, or abuse an organization's cloud credentials.
AnalysisAI
Unauthenticated arbitrary S3 object write in Budibase server (npm/@budibase/server before 3.39.2) lets any remote caller who learns a workspace ID and S3 datasource ID obtain attacker-chosen PutObject presigned URLs signed with the workspace's stored AWS credentials. The /api/attachments/:datasourceId/url route is gated only by reCAPTCHA - no authentication, builder role, or datasource permission check - so an unauthenticated attacker can write arbitrary objects to any bucket the stored credentials can reach. No public exploit identified at time of analysis, though the advisory publishes a step-by-step non-destructive PoC.
Technical ContextAI
Budibase is a low-code internal-tools platform whose Node.js/Koa server (packages/server) exposes a static API for handling attachment uploads to user-configured S3 datasources. The vulnerable route in packages/server/src/api/routes/static.ts attaches only the recaptcha middleware before invoking getSignedUploadURL, which calls sdk.datasources.get(datasourceId, { enriched: true }) to materialize the stored AWS accessKeyId/secretAccessKey and then uses the AWS SDK's getSignedUrl with a PutObjectCommand whose Bucket and Key come straight from the request body. The root cause is a missing authorization layer (broken access control, conceptually CWE-862 Missing Authorization), compounded by the server trusting client-supplied bucket/key parameters rather than constraining them to a configured attachment bucket/prefix. Affected CPE coverage is pkg:npm/@budibase/server.
RemediationAI
Vendor-released patch: @budibase/server 3.39.2 - upgrade immediately, as the fix adds authorized(BUILDER) to the /api/attachments/:datasourceId/url route (see PR https://github.com/Budibase/budibase/pull/18774 and commit https://github.com/Budibase/budibase/commit/d9dbb7f6105373cc88ecacdbcab70c776f7dd6a1). If upgrade must be deferred, block POST /api/attachments/:datasourceId/url at a reverse proxy or WAF for unauthenticated callers (side effect: legitimate attachment uploads via S3 datasources will break until clients are authenticated/whitelisted), and rotate any AWS access keys stored in Budibase S3 datasources since they may already have been used to mint attacker-controlled presigned URLs. As a defense-in-depth measure, scope the IAM policy on stored datasource credentials to a single attachment bucket/prefix with no overwrite of trusted assets, so a residual abuse window cannot pivot into other buckets.
Same technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-39912
GHSA-jj36-r9w3-3pfh