Search

Find a vulnerability

Search criteria

    Related vulnerabilities

    GHSA-JJ36-R9W3-3PFH

    Vulnerability from github – Published: 2026-06-22 23:15 – Updated: 2026-06-22 23:15
    VLAI
    Summary
    Budibase: Unauthenticated S3 signed upload URL generation allows arbitrary writes with stored datasource credentials
    Details

    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.

    Show details on source website

    {
      "affected": [
        {
          "package": {
            "ecosystem": "npm",
            "name": "@budibase/server"
          },
          "ranges": [
            {
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "fixed": "3.39.2"
                }
              ],
              "type": "ECOSYSTEM"
            }
          ]
        }
      ],
      "aliases": [
        "CVE-2026-50136"
      ],
      "database_specific": {
        "cwe_ids": [],
        "github_reviewed": true,
        "github_reviewed_at": "2026-06-22T23:15:10Z",
        "nvd_published_at": null,
        "severity": "HIGH"
      },
      "details": "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.\n\n### Details\n\nThe static route registers the signed upload URL endpoint with only `recaptcha` before the controller:\n\n- `packages/server/src/api/routes/static.ts:44-48`\n\n```ts\n44:  .post(\n45:    \"/api/attachments/:datasourceId/url\",\n46:    recaptcha,\n47:    controller.getSignedUploadURL\n48:  )\n```\n\nThe controller loads the datasource by `datasourceId` with enriched secret values:\n\n- `packages/server/src/api/controllers/static/index.ts:590-598`\n\n```ts\n590:export const getSignedUploadURL = async function (\n591:  ctx: Ctx\u003cGetSignedUploadUrlRequest, GetSignedUploadUrlResponse\u003e\n592:) {\n593:  // Ensure datasource is valid\n594:  let datasource\n595:  try {\n596:    const { datasourceId } = ctx.params\n597:    datasource = await sdk.datasources.get(datasourceId, { enriched: true })\n598:    if (!datasource) {\n```\n\nThe request body controls `bucket` and `key`, and the server signs a PUT URL using the stored datasource credentials:\n\n- `packages/server/src/api/controllers/static/index.ts:609-629`\n\n```ts\n609:  if (datasource?.source === \"S3\") {\n610:    const { bucket, key } = ctx.request.body || {}\n611:    if (!bucket || !key) {\n612:      ctx.throw(400, \"bucket and key values are required\")\n613:    }\n614:    try {\n615:      let endpoint = datasource?.config?.endpoint\n616:      if (endpoint \u0026\u0026 !utils.urlHasProtocol(endpoint)) {\n617:        endpoint = `https://${endpoint}`\n618:      }\n619:      const s3 = new S3({\n620:        region: awsRegion,\n621:        endpoint: endpoint,\n622:        credentials: {\n623:          accessKeyId: datasource?.config?.accessKeyId as string,\n624:          secretAccessKey: datasource?.config?.secretAccessKey as string,\n625:        },\n626:      })\n627:      const params = { Bucket: bucket, Key: key }\n628:      signedUrl = await getSignedUrl(s3, new PutObjectCommand(params))\n629:      if (endpoint) {\n```\n\nThe endpoint returns the signed URL and public URL to the caller:\n\n- `packages/server/src/api/controllers/static/index.ts:630-639`\n\n```ts\n630:        publicUrl = `${endpoint}/${bucket}/${key}`\n631:      } else {\n632:        publicUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com/${key}`\n633:      }\n634:    } catch (error: any) {\n635:      ctx.throw(400, error)\n636:    }\n637:  }\n638:\n639:  ctx.body = { signedUrl, publicUrl }\n```\n\nBecause no authorization middleware is applied, the API trusts public input to choose where the stored S3 credentials will write.\n\n### PoC\n\nNon-destructive validation approach:\n\n1. Create or identify a workspace with an S3 datasource.\n2. Obtain the production workspace ID and S3 datasource ID.\n3. Send an unauthenticated request with the workspace ID header and attacker-controlled bucket/key:\n\n```http\nPOST /api/attachments/\u003cdatasourceId\u003e/url HTTP/1.1\nx-budibase-app-id: app_\u003cworkspace-id\u003e\ncontent-type: application/json\n\n{\"bucket\":\"attacker-controlled-or-permitted-bucket\",\"key\":\"poc/budibase.txt\"}\n```\n\n4. Observe that the response contains a signed PUT URL.\n5. Upload harmless content to the returned `signedUrl` and confirm the object is created using the datasource\u0027s stored S3 credentials.\n\n### Impact\n\nThis 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\u0027s cloud credentials.",
      "id": "GHSA-jj36-r9w3-3pfh",
      "modified": "2026-06-22T23:15:10Z",
      "published": "2026-06-22T23:15:10Z",
      "references": [
        {
          "type": "WEB",
          "url": "https://github.com/Budibase/budibase/security/advisories/GHSA-jj36-r9w3-3pfh"
        },
        {
          "type": "WEB",
          "url": "https://github.com/Budibase/budibase/pull/18774"
        },
        {
          "type": "WEB",
          "url": "https://github.com/Budibase/budibase/commit/d9dbb7f6105373cc88ecacdbcab70c776f7dd6a1"
        },
        {
          "type": "PACKAGE",
          "url": "https://github.com/Budibase/budibase"
        }
      ],
      "schema_version": "1.4.0",
      "severity": [
        {
          "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L",
          "type": "CVSS_V3"
        }
      ],
      "summary": "Budibase: Unauthenticated S3 signed upload URL generation allows arbitrary writes with stored datasource credentials"
    }