Skip to content

Commit f6c9cdb

Browse files
fix(security): close the triggerType rate-limit bypass on workflow execute
Caller-supplied triggerType flowed unchecked into preprocessExecution, whose checkRateLimit default turns OFF for 'manual'/'chat' — so any API-key caller, and any anonymous public-API caller billed to the workspace owner, could execute unthrottled by sending {"triggerType":"manual"} (async runs also skipped the worker-side check via admissionCompleted). External callers may now only send the redundant 'api' value; internal JWT callers ('workflow'/'mcp') are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
1 parent 9ee4099 commit f6c9cdb

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

apps/sim/app/api/workflows/[id]/execute/route.async.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,4 +1269,60 @@ describe('workflow execute async route', () => {
12691269
: executionCall.snapshot
12701270
expect(snapshot.metadata.enforceCredentialAccess).toBe(true)
12711271
})
1272+
describe('triggerType override gate', () => {
1273+
it.each([
1274+
['personal API key', EXECUTION_CALLERS[1]],
1275+
['workspace API key', EXECUTION_CALLERS[2]],
1276+
['public API', EXECUTION_CALLERS[3]],
1277+
] as const)(
1278+
'rejects caller-supplied triggerType "manual" from %s callers',
1279+
async (_name, caller) => {
1280+
configureExecutionCaller(caller)
1281+
const req = createMockRequest(
1282+
'POST',
1283+
{ hello: 'world', triggerType: 'manual' },
1284+
{ 'Content-Type': 'application/json', ...caller.headers }
1285+
)
1286+
1287+
const response = await POST(req, { params: Promise.resolve({ id: 'workflow-1' }) })
1288+
1289+
expect(response.status).toBe(400)
1290+
await expect(response.json()).resolves.toMatchObject({
1291+
error: 'External callers cannot override triggerType',
1292+
})
1293+
expect(mockPreprocessExecution).not.toHaveBeenCalled()
1294+
}
1295+
)
1296+
1297+
it('accepts the redundant explicit "api" triggerType from API-key callers', async () => {
1298+
const caller = EXECUTION_CALLERS[1]
1299+
configureExecutionCaller(caller)
1300+
const req = createMockRequest(
1301+
'POST',
1302+
{ hello: 'world', triggerType: 'api' },
1303+
{ 'Content-Type': 'application/json', ...caller.headers, 'X-Execution-Mode': 'async' }
1304+
)
1305+
1306+
const response = await POST(req, { params: Promise.resolve({ id: 'workflow-1' }) })
1307+
1308+
expect(response.status).toBe(202)
1309+
})
1310+
1311+
it('still allows internal JWT callers to set triggerType', async () => {
1312+
const caller = EXECUTION_CALLERS[4]
1313+
configureExecutionCaller(caller)
1314+
const req = createMockRequest(
1315+
'POST',
1316+
{ hello: 'world', triggerType: 'workflow' },
1317+
{ 'Content-Type': 'application/json', ...caller.headers, 'X-Execution-Mode': 'async' }
1318+
)
1319+
1320+
const response = await POST(req, { params: Promise.resolve({ id: 'workflow-1' }) })
1321+
1322+
expect(response.status).toBe(202)
1323+
expect(mockPreprocessExecution).toHaveBeenCalledWith(
1324+
expect.objectContaining({ triggerType: 'workflow' })
1325+
)
1326+
})
1327+
})
12721328
})

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,24 @@ async function handleExecutePost(
730730
)
731731
}
732732

733+
/**
734+
* External callers may not override the trigger type: `manual`/`chat` turn
735+
* rate limiting off entirely (`preprocessExecution` defaults `checkRateLimit`
736+
* from the trigger type), so a caller-supplied value is a quota bypass.
737+
* `'api'` (the value they would get anyway) stays accepted for compatibility
738+
* with callers that send it redundantly.
739+
*/
740+
if (
741+
(auth.authType === AuthType.API_KEY || isPublicApiAccess) &&
742+
body.triggerType !== undefined &&
743+
body.triggerType !== 'api'
744+
) {
745+
return NextResponse.json(
746+
{ error: 'External callers cannot override triggerType' },
747+
{ status: 400 }
748+
)
749+
}
750+
733751
if (auth.authType === 'api_key') {
734752
if (isClientSession) {
735753
return NextResponse.json(

0 commit comments

Comments
 (0)