GHSA-gh4h-34gr-87r7 CVE-2026-73308
Budibase: OAuth2 Token Disclosure via Automation Test Results Broadcast to Other Builders
- Severity
- 5.7 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.41%
- Chance of exploitation in the next 30 days. Higher than 35.03% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
- Fix
- None yet
- No fixed version published
Description
Summary
When an SSO-authenticated user tests an automation in the Budibase builder, their OAuth2 access token and refresh token are included in the automation test results. These results are broadcast via WebSocket to all builders connected to the same dev app and stored in an in-memory cache accessible to any builder who polls the test status endpoint. This allows any co-builder of the same app to steal the testing user's OAuth2 tokens.
Details
The vulnerability exists because getUserContextBindings() intentionally includes OAuth2 tokens in user context bindings (so automations can call external APIs), but the automation test pipeline exposes the full result — including these tokens — to all builders of the same app without sanitization.
Step 1: Tokens included in user bindings
In packages/server/src/sdk/users/utils.ts:134-161:
export function getUserContextBindings(user: ContextUser): UserBindings {
const bindings: UserBindings = {
_id: user._id,
email: user.email,
// ...
}
if (isSSOUser(user) && user.oauth2) {
bindings.oauth2 = {
accessToken: user.oauth2.accessToken, // <-- sensitive
refreshToken: user.oauth2.refreshToken, // <-- sensitive
}
}
return bindings
}
Step 2: Bindings passed to automation execution
In packages/server/src/api/controllers/automation.ts:311-312:
const user = sdk.users.getUserContextBindings(ctx.user)
return await triggers.externalTrigger(
{ ...automation, disabled: false },
{ ...input, appId, user }, // user with tokens passed as event param
{ getResponses: true, onProgress: emitProgress }
)
Step 3: Tokens placed in trigger outputs
In packages/server/src/threads/automation.ts:409-413:
const trigger: AutomationTriggerResult = {
id: data.automation.definition.trigger.id,
stepId: data.automation.definition.trigger.stepId,
inputs: null,
outputs: data.event, // data.event includes user.oauth2 tokens
}
Step 4: Result broadcast without sanitization
The full result (including trigger.outputs.user.oauth2) is exposed via two vectors:
-
WebSocket broadcast —
builderSocket.emitToRoom()callsthis.io.in(room).emit()(packages/server/src/websockets/websocket.ts:291) which sends to ALL sockets in the app's room, not just the originator. -
Test status endpoint —
recordTestProgress()stores the result in a Map keyed by${appId}:${automationId}with no user isolation (packages/server/src/automations/testProgress.ts:41-74). Any builder can callGET /api/automations/:id/test/statusto retrieve another user's test results.
PoC
Requires two builder-level users on the same Budibase app, where User A authenticates via SSO/OIDC (Google, Azure AD, etc.) which provides OAuth2 tokens.
# Step 1: User A (SSO-authenticated builder) tests an automation asynchronously
curl -X POST http://localhost:10000/api/automations/<automation-id>/test?async=true \
-H 'x-budibase-app-id: app_dev_<appid>' \
-H 'Cookie: budibase:auth=<userA_session>' \
-H 'Content-Type: application/json' \
-d '{"row": {"tableId": "ta_xxx"}}'
# Returns: {"message": "Automation test started"}
# Step 2: User B (another builder on the same app) polls the test status
curl -X GET http://localhost:10000/api/automations/<automation-id>/test/status \
-H 'x-budibase-app-id: app_dev_<appid>' \
-H 'Cookie: budibase:auth=<userB_session>'
# Response includes the full automation result with:
# result.trigger.outputs.user.oauth2.accessToken = "ya29.a0AfH6SM..."
# result.trigger.outputs.user.oauth2.refreshToken = "1//0eXyz..."
Additionally, User B can passively receive the tokens by simply having the Budibase builder open (connected via WebSocket), as the BuilderSocketEvent.AutomationTestProgress event with status: "complete" includes the full result payload.
Impact
- OAuth2 access tokens for external services (Google Workspace, Azure AD, GitHub, etc.) are exposed to co-builders of the same app. These tokens can be used to access external APIs as the victim user.
- OAuth2 refresh tokens provide persistent access — an attacker can generate new access tokens even after the original expires, maintaining long-term access to the victim's external service accounts.
- The attack is passive via WebSocket — an attacker only needs to have the builder UI open to receive tokens when any co-builder tests an automation.
- Test results persist in memory for 5 minutes (TTL in
testProgress.ts:15), providing a window for polling-based attacks.
Recommended Fix
Strip OAuth2 tokens from automation test results before storing/broadcasting them. The tokens are needed during automation execution but should not be included in the result sent to clients.
In packages/server/src/api/controllers/automation.ts, sanitize the result before passing to emitProgress:
function sanitizeAutomationResult(result: AutomationResults): AutomationResults {
const sanitized = cloneDeep(result)
if (sanitized.trigger?.outputs?.user?.oauth2) {
delete sanitized.trigger.outputs.user.oauth2
}
for (const step of sanitized.steps || []) {
if (step.outputs?.user?.oauth2) {
delete step.outputs.user.oauth2
}
}
return sanitized
}
Apply sanitization in the emitProgress callback at line 290 and before returning ctx.body at line 342:
emitProgress({
status: "complete",
occurredAt: Date.now(),
result: sanitizeAutomationResult(result),
})
Additionally, the testProgress store should be scoped per-user (keyed by ${appId}:${automationId}:${userId}) so that one builder's test results are not accessible to another builder via the testStatus endpoint.
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| @budibase/server | <= 3.38.1 |
No fix yet |
How to fix
No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.
Severity breakdown
CVSS 3.1 5.7
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- Required
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
How to prevent it
- Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-6x9p-4r67-5gjx Budibase authenticated arbitrary S3 signed upload URL issuance via `/api/attachments/:datasourceId/url` | High | Also affects @budibase/server |
| GHSA-5fpj-28rv-84r7 Budibase: SSRF in Automation Steps - Webhook, Zapier, N8N, Slack, Discord Bypass IP Blacklist | High | Also affects @budibase/server |
| GHSA-hfhx-w8p8-4hc7 Budibase: SSRF via bare fetch() in uploadUrl during AI table generation | Medium | Also affects @budibase/server |
| GHSA-v42f-v8xc-j435 Budibase: SSRF via DNS rebinding in the REST datasource integration | High | Also affects @budibase/server |
| GHSA-pmpg-2mxq-6xwr Budibase: NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete | High | Also affects @budibase/server |
| GHSA-cr7p-cr3q-h5cm Budibase: Account Enumeration via Login Lockout Response Differential | Medium | Also affects @budibase/server |
| GHSA-pvcr-8mvp-w8qr Budibase: Chat-Link Handoff Identity Confusion (Same-Tenant Account-Link CSRF) | High | Also affects @budibase/server |
| GHSA-2xgg-r2wc-c5r2 Budibase: MySQL DESCRIBE Backtick Injection via multipleStatements in Database Connector | High | Also affects @budibase/server |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor