Skip to content

Commit 63fcfe2

Browse files
feat(infra): CORS + CSP coverage for the v2 execute path
/api/v2/workflows/:id/execute gets the same wildcard-origin, credential-free CORS policy as v1 (the default credentialed policy would block browser API-key calls and open a cookie CSRF surface) with X-Sim-Stream-Protocol allowed and no X-Execution-Mode (async is body-selected on v2), plus the COEP/COOP/CSP header block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
1 parent c411f6e commit 63fcfe2

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

apps/sim/next.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,17 @@ const nextConfig: NextConfig = {
254254
},
255255
],
256256
},
257+
{
258+
source: '/api/v2/workflows/:id/execute',
259+
headers: [
260+
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
261+
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
262+
{
263+
key: 'Content-Security-Policy',
264+
value: getWorkflowExecutionCSPPolicy(),
265+
},
266+
],
267+
},
257268
{
258269
// Exclude Vercel internal resources and static assets from strict COEP, Google Drive Picker
259270
// and the /demo Cal.com booking embed to prevent 'refused to connect' / slow-load issues

apps/sim/proxy.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,29 @@ describe('resolveApiCorsPolicy', () => {
9090
expect(policy.headers).toContain('X-Execution-Id')
9191
})
9292

93+
it('serves v2 workflow execute with wildcard origin and the stream-protocol header', () => {
94+
const policy = resolveApiCorsPolicy(
95+
makeRequest('/api/v2/workflows/workflow-123/execute', 'https://other.example')
96+
)
97+
expect(policy.origin).toBe('*')
98+
expect(policy.credentials).toBe(false)
99+
expect(policy.headers).toContain('X-Execution-Id')
100+
expect(policy.headers).toContain('X-Sim-Stream-Protocol')
101+
// Async is body-selected on v2 — the mode header is deliberately absent.
102+
expect(policy.headers).not.toContain('X-Execution-Mode')
103+
})
104+
105+
it('does not match the v2 execute rule for nested or executions paths', () => {
106+
const nested = resolveApiCorsPolicy(
107+
makeRequest('/api/v2/workflows/workflow-123/execute/extra', 'https://other.example')
108+
)
109+
expect(nested.origin).toBe('https://app.sim.test')
110+
const executions = resolveApiCorsPolicy(
111+
makeRequest('/api/v2/workflows/workflow-123/executions/e-1', 'https://other.example')
112+
)
113+
expect(executions.origin).toBe('https://app.sim.test')
114+
})
115+
93116
it('does not match the workflow execute rule for nested paths', () => {
94117
const policy = resolveApiCorsPolicy(
95118
makeRequest('/api/workflows/workflow-123/execute/extra', 'https://other.example')
@@ -113,6 +136,7 @@ describe('resolveApiCorsPolicy', () => {
113136
'/api/mcp/copilot',
114137
'/api/chat/abc',
115138
'/api/workflows/wf/execute',
139+
'/api/v2/workflows/wf/execute',
116140
'/api/files/upload',
117141
]
118142
for (const path of paths) {

apps/sim/proxy.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const DEFAULT_API_ALLOWED_HEADERS =
2323
const WORKFLOW_EXECUTE_HEADERS =
2424
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key, X-Execution-Id'
2525

26+
/** v2 execute: async is body-selected (no X-Execution-Mode) and streaming negotiates X-Sim-Stream-Protocol. */
27+
const WORKFLOW_EXECUTE_V2_HEADERS = `${WORKFLOW_EXECUTE_HEADERS}, X-Sim-Stream-Protocol`
28+
2629
/** Subpaths under /api/chat/* that serve the workspace UI, not embeds. */
2730
const EMBED_RESERVED_SEGMENTS = new Set(['manage', 'validate'])
2831

@@ -82,6 +85,18 @@ const CORS_RULES: readonly CorsRule[] = [
8285
headers: WORKFLOW_EXECUTE_HEADERS,
8386
}),
8487
},
88+
{
89+
// Mirrors the v1 rule: public execute endpoints are wildcard-origin and
90+
// credential-free — the default credentialed policy would both block
91+
// browser API-key calls and open a cookie-bearing CSRF surface.
92+
match: (p) => /^\/api\/v2\/workflows\/[^/]+\/execute$/.test(p),
93+
policy: () => ({
94+
origin: '*',
95+
credentials: false,
96+
methods: 'POST,OPTIONS',
97+
headers: WORKFLOW_EXECUTE_V2_HEADERS,
98+
}),
99+
},
85100
]
86101

87102
/** Single source of truth for /api/* CORS — resolved at request time, not baked at build. */

0 commit comments

Comments
 (0)