From 148e4b8b9d62bdac243d6cc0790e41f2ce3cc455 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 15 Jun 2026 12:52:56 -0700 Subject: [PATCH] Revert "fix(execute): block cross-origin session-authenticated workflow runs (#5062)" This reverts commit 67e02fab3a571fd2aafdee5177eafc3e458b6e45. --- .../[id]/execute/route.async.test.ts | 20 ------- .../app/api/workflows/[id]/execute/route.ts | 12 ----- .../sim/lib/core/security/same-origin.test.ts | 53 ------------------- apps/sim/lib/core/security/same-origin.ts | 36 ------------- 4 files changed, 121 deletions(-) delete mode 100644 apps/sim/lib/core/security/same-origin.test.ts delete mode 100644 apps/sim/lib/core/security/same-origin.ts diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 87715f0b514..ebce3426622 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -194,26 +194,6 @@ describe('workflow execute async route', () => { ) }) - it('rejects cross-origin session requests before authorization work', async () => { - const req = createMockRequest( - 'POST', - { input: { hello: 'world' } }, - { - 'Content-Type': 'application/json', - 'Sec-Fetch-Site': 'cross-site', - } - ) - const params = Promise.resolve({ id: 'workflow-1' }) - - const response = await POST(req, { params }) - const body = await response.json() - - expect(response.status).toBe(403) - expect(body.error).toBe('Access denied') - expect(mockAuthorizeWorkflowByWorkspacePermission).not.toHaveBeenCalled() - expect(mockEnqueue).not.toHaveBeenCalled() - }) - it('rejects oversized request bodies before authorization work', async () => { const req = createMockRequest( 'POST', diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 6b048ac144a..34868195dc1 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -20,7 +20,6 @@ import { getTimeoutErrorMessage, isTimeoutError, } from '@/lib/core/execution-limits' -import { isCrossOriginSessionRequest } from '@/lib/core/security/same-origin' import { generateRequestId } from '@/lib/core/utils/request' import { SSE_HEADERS } from '@/lib/core/utils/sse' import { @@ -394,17 +393,6 @@ async function handleExecutePost( try { const auth = await checkHybridAuth(req, { requireWorkflowId: false }) - - // CSRF guard: reject session-cookie execution that is provably cross-origin - // (a different site driving the user's browser). Scoped to session auth — - // API-key / public-API / internal-JWT callers don't use cookies. This is not - // a defense against a non-browser client forging headers; that surface is - // covered by the credit and execution rate-limit gates. - if (auth.success && auth.authType === AuthType.SESSION && isCrossOriginSessionRequest(req)) { - reqLogger.warn('Rejected cross-origin session-authenticated execute request') - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) - } - const isMcpBridgeRequest = auth.authType === AuthType.INTERNAL_JWT && req.headers.get(MCP_TOOL_BRIDGE_HEADER) === 'true' const useMcpBridgeAuthenticatedUserAsActor = diff --git a/apps/sim/lib/core/security/same-origin.test.ts b/apps/sim/lib/core/security/same-origin.test.ts deleted file mode 100644 index 4a73e6050bf..00000000000 --- a/apps/sim/lib/core/security/same-origin.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @vitest-environment node - */ -import type { NextRequest } from 'next/server' -import { describe, expect, it } from 'vitest' -import { isCrossOriginSessionRequest } from '@/lib/core/security/same-origin' -import { getBaseUrl } from '@/lib/core/utils/urls' - -function makeRequest(headers: Record): NextRequest { - return { headers: new Headers(headers) } as unknown as NextRequest -} - -describe('isCrossOriginSessionRequest', () => { - it('allows a same-origin browser fetch', () => { - expect(isCrossOriginSessionRequest(makeRequest({ 'sec-fetch-site': 'same-origin' }))).toBe( - false - ) - }) - - it('rejects same-site requests (sibling subdomains are not our origin)', () => { - expect(isCrossOriginSessionRequest(makeRequest({ 'sec-fetch-site': 'same-site' }))).toBe(true) - }) - - it('rejects cross-site requests', () => { - expect(isCrossOriginSessionRequest(makeRequest({ 'sec-fetch-site': 'cross-site' }))).toBe(true) - }) - - it('rejects navigations not initiated from our front-end', () => { - expect(isCrossOriginSessionRequest(makeRequest({ 'sec-fetch-site': 'none' }))).toBe(true) - }) - - it('falls back to the Origin header when Sec-Fetch-Site is absent (same-origin allowed)', () => { - const origin = new URL(getBaseUrl()).origin - expect(isCrossOriginSessionRequest(makeRequest({ origin }))).toBe(false) - }) - - it('rejects a foreign Origin when Sec-Fetch-Site is absent', () => { - expect(isCrossOriginSessionRequest(makeRequest({ origin: 'https://evil.example.com' }))).toBe( - true - ) - }) - - it('allows requests where the origin cannot be determined (no Sec-Fetch-Site, no Origin)', () => { - expect(isCrossOriginSessionRequest(makeRequest({}))).toBe(false) - }) - - it('trusts the unforgeable Sec-Fetch-Site over a spoofed same-origin Origin', () => { - const origin = new URL(getBaseUrl()).origin - expect( - isCrossOriginSessionRequest(makeRequest({ 'sec-fetch-site': 'cross-site', origin })) - ).toBe(true) - }) -}) diff --git a/apps/sim/lib/core/security/same-origin.ts b/apps/sim/lib/core/security/same-origin.ts deleted file mode 100644 index ab976f0ca51..00000000000 --- a/apps/sim/lib/core/security/same-origin.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { NextRequest } from 'next/server' -import { isSameOrigin } from '@/lib/core/utils/validation' - -/** - * Returns true when a request is provably cross-origin — a browser fetch driven - * from a different site than our own. Used to reject session-cookie CSRF on - * state-changing routes: a cross-site browser request always carries - * `Sec-Fetch-Site: cross-site` or a mismatched `Origin`, and neither header can - * be set by in-browser attacker JavaScript (both are forbidden headers). - * - * `Sec-Fetch-Site` is the primary signal; only `same-origin` is treated as our - * own front-end. The app is single-origin, so `same-site` (sibling subdomains), - * `cross-site`, and `none` are all rejected. When it is absent, fall back to an - * `Origin` same-origin check. When neither header is present the origin cannot - * be determined, so the request is allowed — a genuine cross-site browser attack - * cannot omit these headers. - * - * This is CSRF protection only. It does not defend against a non-browser client - * that forges headers directly (no header-based check can); that surface is - * covered by the credit and execution rate-limit gates. - */ -export function isCrossOriginSessionRequest(req: NextRequest): boolean { - const secFetchSite = req.headers.get('sec-fetch-site') - if (secFetchSite) { - return secFetchSite !== 'same-origin' - } - - const origin = req.headers.get('origin') - if (!origin) return false - - try { - return !isSameOrigin(origin) - } catch { - return false - } -}