Root cause
AI change
[BUDI-9240] Make CurrentUser.oauth available in automation external data connector and API request steps
Loading…
How AI contributed
Direct introductionWhen 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.
Root cause
[BUDI-9240] Make CurrentUser.oauth available in automation external data connector and API request steps
Fix
Sanitize automation test OAuth outputs
Code comparison
--- a/packages/server/src/automations/steps/apiRequest.ts+++ b/packages/server/src/automations/steps/apiRequest.ts@@ -5,16 +5,19 @@ import { type APIRequestStepInputs, type APIRequestStepOutputs, ContextEmitter,+ AutomationContext, } from "@budibase/types" export async function run({ inputs, appId, emitter,+ context, }: { inputs: APIRequestStepInputs appId: string emitter: ContextEmitter+ context?: AutomationContext }): Promise<APIRequestStepOutputs> { if (inputs.query == null) { return {@@ -34,6 +37,7 @@ export async function run({ params: { queryId, },+ user: context?.user, }) try {--- a/packages/server/src/automations/steps/executeQuery.ts+++ b/packages/server/src/automations/steps/executeQuery.ts@@ -5,16 +5,19 @@ import { ContextEmitter, ExecuteQueryStepInputs, ExecuteQueryStepOutputs,+ AutomationContext, } from "@budibase/types" export async function run({ inputs, appId, emitter,+ context, }: { inputs: ExecuteQueryStepInputs appId: string emitter: ContextEmitter+ context?: AutomationContext }): Promise<ExecuteQueryStepOutputs> { if (inputs.query == null) { return {@@ -34,6 +37,7 @@ export async function run({ params: { queryId, },+ user: context?.user, }) try {--- a/packages/server/src/automations/steps/utils.ts+++ b/packages/server/src/automations/steps/utils.ts@@ -1,4 +1,4 @@-import { ContextEmitter } from "@budibase/types"+import { ContextEmitter, ContextUser } from "@budibase/types" export async function getFetchResponse(fetched: any) { let status = fetched.status,@@ -19,7 +19,7 @@ export async function getFetchResponse(fetched: any) { // need to make sure all ctx structures have the // throw added to them, so that controllers don't // throw a ctx.throw undefined when error occurs-// opts can contain, body, params and version+// opts can contain, body, params, version, and user export function buildCtx( appId: string, emitter?: ContextEmitter | null,@@ -27,7 +27,7 @@ export function buildCtx( ) { const ctx: any = { appId,- user: { appId },+ user: opts.user || { appId }, eventEmitter: emitter, throw: (code: string, error: any) => { throw error--- a/packages/server/src/api/controllers/automation.ts+++ b/packages/server/src/api/controllers/automation.ts@@ -40,6 +40,7 @@ import { } from "@budibase/types" import { testConnection } from "../../automations/email" import { getActionDefinitions as actionDefs } from "../../automations/actions"+import { sanitizeAutomationTestResult } from "../../automations/sanitizeTestResult" import * as triggers from "../../automations/triggers" import { AutomationTestProgressEvent,@@ -395,10 +396,11 @@ export async function test( const emitProgress = (event: ProgressEventInput) => { const payload: AutomationTestProgressEvent = { ...event,+ result: sanitizeAutomationTestResult(event.result), automationId: automation._id!, appId, }- recordTestProgress(appId, automation._id!, payload)+ recordTestProgress(appId, automation._id!, payload, ctx.user._id) builderSocket?.emitToRoom( ctx, ctx.appId,@@ -421,15 +423,16 @@ export async function test( ) }) await events.automation.tested(automation)+ const sanitizedResult = sanitizeAutomationTestResult(result) emitProgress({ status: "complete", occurredAt: Date.now(),- result,+ result: sanitizedResult, })- return result+ return sanitizedResult } - clearTestProgress(appId, automation._id!)+ clearTestProgress(appId, automation._id!, ctx.user._id) if (asyncFlag) { ctx.status = 202@@ -449,6 +452,6 @@ export async function test( export async function testStatus(ctx: UserCtx<void, unknown>) { const automationId = ctx.params.id- const status = getTestProgress(ctx.appId, automationId)+ const status = getTestProgress(ctx.appId, automationId, ctx.user._id) ctx.body = status || {} }--- /dev/null+++ b/packages/server/src/automations/sanitizeTestResult.ts@@ -0,0 +1,31 @@+import { AutomationTestProgressEvent } from "@budibase/types"+import cloneDeep from "lodash/cloneDeep"++function stripOAuth2FromOutputs(+ outputs?: Record<string, any> | null+): Record<string, any> | undefined | null {+ if (outputs?.user?.oauth2) {+ delete outputs.user.oauth2+ }+ return outputs+}++export function sanitizeAutomationTestResult(+ result: AutomationTestProgressEvent["result"]+): AutomationTestProgressEvent["result"] {+ if (!result) {+ return result+ }++ const sanitized: NonNullable<AutomationTestProgressEvent["result"]> =+ cloneDeep(result)+ if ("trigger" in sanitized) {+ stripOAuth2FromOutputs(sanitized.trigger.outputs)+ for (const step of sanitized.steps || []) {+ stripOAuth2FromOutputs(step.outputs)+ }+ } else {+ stripOAuth2FromOutputs(sanitized.outputs)+ }+ return sanitized+}--- a/packages/server/src/automations/testProgress.ts+++ b/packages/server/src/automations/testProgress.ts@@ -35,15 +35,19 @@ export function stopCleanup() { clearInterval(cleanupInterval) } -const getKey = (appId: string | undefined, automationId: string) =>- `${appId || "unknown"}:${automationId}`+const getKey = (+ appId: string | undefined,+ automationId: string,+ userId?: string+) => `${appId || "unknown"}:${automationId}:${userId || "unknown"}` export function recordTestProgress( appId: string | undefined, automationId: string,- event: AutomationTestProgressEvent+ event: AutomationTestProgressEvent,+ userId?: string ) {- const key = getKey(appId, automationId)+ const key = getKey(appId, automationId, userId) const state = progressState.get(key) || ({@@ -75,16 +79,18 @@ export function recordTestProgress( export function getTestProgress( appId: string | undefined,- automationId: string+ automationId: string,+ userId?: string ) {- const key = getKey(appId, automationId)+ const key = getKey(appId, automationId, userId) return progressState.get(key) } export function clearTestProgress( appId: string | undefined,- automationId: string+ automationId: string,+ userId?: string ) {- const key = getKey(appId, automationId)+ const key = getKey(appId, automationId, userId) progressState.delete(key) }Candidate 1d5ef3e9273cadc3b953df7a362af892c9ca19b08a999b03232a5e0eac6bcf07 · Fix 39f857b123c6cf432f91328ccc678b5acb3fc87006d78346512a9f637f4c7455
Releases
Advisory references