diff --git a/apps/sim/app/api/auth/instagram/authorize/route.test.ts b/apps/sim/app/api/auth/instagram/authorize/route.test.ts index 12e6415d52f..5340ef5adb7 100644 --- a/apps/sim/app/api/auth/instagram/authorize/route.test.ts +++ b/apps/sim/app/api/auth/instagram/authorize/route.test.ts @@ -1,4 +1,5 @@ import { createMockRequest } from '@sim/testing' +import { createRouteContext } from '@sim/testing/helpers/http' import { authMockFns } from '@sim/testing/mocks/auth.mock' import { oauthUtilsMock, oauthUtilsMockFns } from '@sim/testing/mocks/oauth-utils.mock' import { urlsMockFns } from '@sim/testing/mocks/urls.mock' @@ -45,7 +46,7 @@ describe('Instagram authorize route', () => { 'https://sim.test/api/auth/instagram/authorize?workspaceId=workspace-1&draftId=draft-exact' ) - const response = await GET(request) + const response = await GET(request, createRouteContext({})) expect(response.status).toBe(307) expect(response.headers.get('set-cookie')).toContain( @@ -54,6 +55,20 @@ describe('Instagram authorize route', () => { expect(mocks.createCredentialConnection).not.toHaveBeenCalled() }) + it('refuses a cross-site workspace launch before creating a connection draft', async () => { + const request = createMockRequest( + 'GET', + undefined, + { 'sec-fetch-site': 'cross-site' }, + 'https://sim.test/api/auth/instagram/authorize?workspaceId=workspace-1' + ) + + const response = await GET(request, createRouteContext({})) + + expect(response.status).toBe(403) + expect(mocks.createCredentialConnection).not.toHaveBeenCalled() + }) + it('returns a conflict when a different connection intent is already active', async () => { mocks.createCredentialConnection.mockRejectedValue( new OrchestrationError( @@ -68,7 +83,7 @@ describe('Instagram authorize route', () => { 'https://sim.test/api/auth/instagram/authorize?workspaceId=workspace-1' ) - const response = await GET(request) + const response = await GET(request, createRouteContext({})) expect(response.status).toBe(409) await expect(response.json()).resolves.toEqual({ diff --git a/apps/sim/app/api/auth/instagram/authorize/route.ts b/apps/sim/app/api/auth/instagram/authorize/route.ts index 17f21e99e66..58c78a2d3f5 100644 --- a/apps/sim/app/api/auth/instagram/authorize/route.ts +++ b/apps/sim/app/api/auth/instagram/authorize/route.ts @@ -6,6 +6,7 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { requireConfiguredOAuthClient } from '@/lib/core/config/env-capabilities.server' import { asOrchestrationError } from '@/lib/core/orchestration/types' +import { isCrossSiteSessionRequest } from '@/lib/core/security/same-origin' import { getBaseUrl } from '@/lib/core/utils/urls' import { isSameOrigin } from '@/lib/core/utils/validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -41,6 +42,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { let credentialDraftId = draftId if (workspaceId && !draftId) { + // The query alone picks the workspace, so a cross-site page must not be + // able to start this launch with the viewer's session cookie. + if (isCrossSiteSessionRequest(request)) { + logger.warn('Rejected cross-site Instagram workspace launch', { userId: session.user.id }) + return NextResponse.json({ error: 'Cross-site launch refused' }, { status: 403 }) + } try { const connection = await createCredentialConnection.execute({ principal: { kind: 'session', userId: session.user.id, sessionId }, diff --git a/scripts/check-test-types.baseline.json b/scripts/check-test-types.baseline.json index e24d2c69d44..9188621c97b 100644 --- a/scripts/check-test-types.baseline.json +++ b/scripts/check-test-types.baseline.json @@ -14,9 +14,6 @@ "apps/sim/app/api/auth/forget-password/route.test.ts": { "TS2554": 8 }, - "apps/sim/app/api/auth/instagram/authorize/route.test.ts": { - "TS2554": 2 - }, "apps/sim/app/api/auth/oauth/credentials/route.test.ts": { "TS2322": 1, "TS2345": 2,