---
id: CVE-2026-50136
title: >-
  Budibase: Unauthenticated S3 signed upload URL generation allows arbitrary
  writes with stored datasource credentials
summary: >-
  Budibase: Unauthenticated S3 signed upload URL generation allows arbitrary
  writes with stored datasource credentials
severity: high
cvss: 7.4
vendor: budibase
product: '@budibase/server'
ecosystem: npm
affected:
  - '@budibase/server < 3.39.2'
patched:
  - '@budibase/server 3.39.2'
published: '2026-06-22'
updated: '2026-06-22'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-jj36-r9w3-3pfh'
references:
  - url: >-
      https://github.com/Budibase/budibase/security/advisories/GHSA-jj36-r9w3-3pfh
  - url: 'https://github.com/Budibase/budibase/pull/18774'
  - url: >-
      https://github.com/Budibase/budibase/commit/d9dbb7f6105373cc88ecacdbcab70c776f7dd6a1
  - url: 'https://github.com/advisories/GHSA-jj36-r9w3-3pfh'
tags:
  - ghsa
  - npm
epss: 0.0029
epssPercentile: 0.19181
ingestedAt: '2026-06-29T13:24:35.490Z'
---

## Overview

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`

```ts
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`

```ts
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`

```ts
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`

```ts
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:

```http
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"}
```

4. Observe that the response contains a signed PUT URL.
5. 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.

## Affected packages

- `@budibase/server < 3.39.2`

## Remediation

Upgrade to a patched release:

- `@budibase/server 3.39.2`
