Skip to content

Unauthenticated S3 signed upload URL generation allows arbitrary writes with stored datasource credentials

High
mjashanks published GHSA-jj36-r9w3-3pfh May 28, 2026

Package

No package listed

Affected versions

<3.39.3

Patched versions

None

Description

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:

  1. Create or identify a workspace with an S3 datasource.
  2. Obtain the production workspace ID and S3 datasource ID.
  3. 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"}
  1. Observe that the response contains a signed PUT URL.
  2. Upload harmless content to the returned signedUrl and 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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L

CVE ID

CVE-2026-50136

Weaknesses

No CWEs

Credits