From 27e3cf836456e5e43ad7483f6ea00004bc9b272b Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 11:08:01 -0700 Subject: [PATCH 1/3] fix(analytics): apply runtime CSP to all landing pages, not just / GTM/GA4 script-src and connect-src allowlist entries are gated behind isHosted, which next.config.ts's build-time CSP bakes in from whatever NEXT_PUBLIC_APP_URL was resolved at build/boot time. Only the homepage route was overridden with the request-time CSP in proxy.ts, so every other landing page (including /demo) silently lost the googletagmanager.com/google-analytics.com allowlist and GTM never fired. --- apps/sim/proxy.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 1d5e0581589..bc491f61ec6 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -283,11 +283,15 @@ export async function proxy(request: NextRequest) { const response = NextResponse.next() response.headers.set('Vary', 'User-Agent') - if (url.pathname === '/') { - response.headers.set('Content-Security-Policy', generateRuntimeCSP()) - response.headers.set('X-Content-Type-Options', 'nosniff') - response.headers.set('X-Frame-Options', 'SAMEORIGIN') - } + // Every remaining matched route (landing/marketing pages, /demo, /pricing, etc.) + // needs the runtime CSP, not next.config.ts's build-time policy — the build-time + // policy bakes `isHosted` from whatever NEXT_PUBLIC_APP_URL was available at + // build/boot time, which silently drops the googletagmanager.com/google-analytics.com + // allowlist entries when that value wasn't resolved yet. Previously only '/' got this + // override, so GTM/GA loaded on the homepage but was CSP-blocked everywhere else. + response.headers.set('Content-Security-Policy', generateRuntimeCSP()) + response.headers.set('X-Content-Type-Options', 'nosniff') + response.headers.set('X-Frame-Options', 'SAMEORIGIN') return track(request, response) } From 363b18d6a14fc0bfc071d8a56d50770b5b56de05 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 11:08:58 -0700 Subject: [PATCH 2/3] style: drop inline comment from proxy CSP fix --- apps/sim/proxy.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index bc491f61ec6..afb3c383b7f 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -283,12 +283,6 @@ export async function proxy(request: NextRequest) { const response = NextResponse.next() response.headers.set('Vary', 'User-Agent') - // Every remaining matched route (landing/marketing pages, /demo, /pricing, etc.) - // needs the runtime CSP, not next.config.ts's build-time policy — the build-time - // policy bakes `isHosted` from whatever NEXT_PUBLIC_APP_URL was available at - // build/boot time, which silently drops the googletagmanager.com/google-analytics.com - // allowlist entries when that value wasn't resolved yet. Previously only '/' got this - // override, so GTM/GA loaded on the homepage but was CSP-blocked everywhere else. response.headers.set('Content-Security-Policy', generateRuntimeCSP()) response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-Frame-Options', 'SAMEORIGIN') From 7cac9f2a8ffe7913a9e798409d8bb827f518d890 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 11:14:37 -0700 Subject: [PATCH 3/3] fix(analytics): apply runtime CSP to authenticated /invite pages too handleInvitationRedirects returns NextResponse.next() for authenticated users, bypassing the catch-all block entirely, so it never picked up the runtime CSP fix in the same way. Same class of gap flagged by review. --- apps/sim/proxy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index afb3c383b7f..48213786dad 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -183,7 +183,11 @@ function handleInvitationRedirects( new URL(`/login?callbackUrl=${callbackParam}&invite_flow=true`, request.url) ) } - return NextResponse.next() + const response = NextResponse.next() + response.headers.set('Content-Security-Policy', generateRuntimeCSP()) + response.headers.set('X-Content-Type-Options', 'nosniff') + response.headers.set('X-Frame-Options', 'SAMEORIGIN') + return response } /**