diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index 4d3e2ee33a..acbb1fbdda 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -29,38 +29,112 @@ export interface CSPDirectives { 'object-src'?: string[] } +/** + * Static CSP sources shared between build-time and runtime. + * Add new domains here — both paths pick them up automatically. + */ +const STATIC_SCRIPT_SRC = [ + "'self'", + "'unsafe-inline'", + 'https://*.google.com', + 'https://apis.google.com', + 'https://assets.onedollarstats.com', + 'https://challenges.cloudflare.com', + ...(isReactGrabEnabled ? ['https://unpkg.com'] : []), + ...(isHosted + ? [ + 'https://www.googletagmanager.com', + 'https://www.google-analytics.com', + 'https://analytics.ahrefs.com', + ] + : []), +] as const + +const STATIC_IMG_SRC = [ + "'self'", + 'data:', + 'blob:', + 'https://*.googleusercontent.com', + 'https://*.google.com', + 'https://*.atlassian.com', + 'https://cdn.discordapp.com', + 'https://*.githubusercontent.com', + 'https://*.s3.amazonaws.com', + 'https://s3.amazonaws.com', + 'https://*.amazonaws.com', + 'https://*.blob.core.windows.net', + 'https://github.com/*', + 'https://collector.onedollarstats.com', + 'https://cursor.com', + ...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []), +] as const + +const STATIC_CONNECT_SRC = [ + "'self'", + 'https://api.browser-use.com', + 'https://api.elevenlabs.io', + 'wss://api.elevenlabs.io', + 'https://api.exa.ai', + 'https://api.firecrawl.dev', + 'https://*.googleapis.com', + 'https://*.amazonaws.com', + 'https://*.s3.amazonaws.com', + 'https://*.blob.core.windows.net', + 'https://*.atlassian.com', + 'https://*.supabase.co', + 'https://api.github.com', + 'https://github.com/*', + 'https://challenges.cloudflare.com', + 'https://collector.onedollarstats.com', + ...(isHosted + ? [ + 'https://www.googletagmanager.com', + 'https://*.google-analytics.com', + 'https://*.analytics.google.com', + 'https://analytics.google.com', + 'https://www.google.com', + ] + : []), +] as const + +const STATIC_FRAME_SRC = [ + "'self'", + 'https://challenges.cloudflare.com', + 'https://drive.google.com', + 'https://docs.google.com', + 'https://*.google.com', + 'https://www.youtube.com', + 'https://player.vimeo.com', + 'https://www.dailymotion.com', + 'https://player.twitch.tv', + 'https://clips.twitch.tv', + 'https://streamable.com', + 'https://fast.wistia.net', + 'https://www.tiktok.com', + 'https://w.soundcloud.com', + 'https://open.spotify.com', + 'https://embed.music.apple.com', + 'https://www.loom.com', + 'https://www.facebook.com', + 'https://www.instagram.com', + 'https://platform.twitter.com', + 'https://rumble.com', + 'https://play.vidyard.com', + 'https://iframe.cloudflarestream.com', + 'https://www.mixcloud.com', + 'https://tenor.com', + 'https://giphy.com', + ...(isHosted ? ['https://www.googletagmanager.com'] : []), +] as const + // Build-time CSP directives (for next.config.ts) export const buildTimeCSPDirectives: CSPDirectives = { 'default-src': ["'self'"], - - 'script-src': [ - "'self'", - "'unsafe-inline'", - "'unsafe-eval'", - 'https://*.google.com', - 'https://apis.google.com', - 'https://assets.onedollarstats.com', - 'https://challenges.cloudflare.com', - ...(isReactGrabEnabled ? ['https://unpkg.com'] : []), - ...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []), - ], - + 'script-src': [...STATIC_SCRIPT_SRC], 'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], 'img-src': [ - "'self'", - 'data:', - 'blob:', - 'https://*.googleusercontent.com', - 'https://*.google.com', - 'https://*.atlassian.com', - 'https://cdn.discordapp.com', - 'https://*.githubusercontent.com', - 'https://*.s3.amazonaws.com', - 'https://s3.amazonaws.com', - 'https://github.com/*', - 'https://collector.onedollarstats.com', - ...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []), + ...STATIC_IMG_SRC, ...(env.S3_BUCKET_NAME && env.AWS_REGION ? [`https://${env.S3_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`] : []), @@ -70,21 +144,16 @@ export const buildTimeCSPDirectives: CSPDirectives = { ...(env.S3_CHAT_BUCKET_NAME && env.AWS_REGION ? [`https://${env.S3_CHAT_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`] : []), - 'https://*.amazonaws.com', - 'https://*.blob.core.windows.net', - 'https://github.com/*', ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL), ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_FAVICON_URL), ], 'media-src': ["'self'", 'blob:'], - 'font-src': ["'self'", 'https://fonts.gstatic.com'], 'connect-src': [ - "'self'", + ...STATIC_CONNECT_SRC, env.NEXT_PUBLIC_APP_URL || '', - // Only include localhost fallbacks in development mode ...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? ['http://localhost:11434'] : []), ...(env.NEXT_PUBLIC_SOCKET_URL ? [ @@ -94,42 +163,12 @@ export const buildTimeCSPDirectives: CSPDirectives = { : isDev ? ['http://localhost:3002', 'ws://localhost:3002'] : []), - 'https://api.browser-use.com', - 'https://api.elevenlabs.io', - 'wss://api.elevenlabs.io', - 'https://api.exa.ai', - 'https://api.firecrawl.dev', - 'https://*.googleapis.com', - 'https://*.amazonaws.com', - 'https://*.s3.amazonaws.com', - 'https://*.blob.core.windows.net', - 'https://*.atlassian.com', - 'https://*.supabase.co', - 'https://api.github.com', - 'https://github.com/*', - 'https://challenges.cloudflare.com', - 'https://collector.onedollarstats.com', - ...(isHosted - ? [ - 'https://www.googletagmanager.com', - 'https://*.google-analytics.com', - 'https://*.analytics.google.com', - ] - : []), ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL), ...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL), ...getHostnameFromUrl(env.NEXT_PUBLIC_TERMS_URL), ], - 'frame-src': [ - "'self'", - 'https://challenges.cloudflare.com', - 'https://drive.google.com', - 'https://docs.google.com', - 'https://*.google.com', - ...(isHosted ? ['https://www.googletagmanager.com'] : []), - ], - + 'frame-src': [...STATIC_FRAME_SRC], 'frame-ancestors': ["'self'"], 'form-action': ["'self'"], 'base-uri': ["'self'"], @@ -152,13 +191,14 @@ export function buildCSPString(directives: CSPDirectives): string { } /** - * Generate runtime CSP header with dynamic environment variables (safer approach) - * This maintains compatibility with existing inline scripts while fixing Docker env var issues + * Generate runtime CSP header with dynamic environment variables. + * Composes from the same STATIC_* constants as buildTimeCSPDirectives, + * but resolves env vars at request time via getEnv() to fix Docker + * deployments where build-time values may be stale placeholders. */ export function generateRuntimeCSP(): string { const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || '' - // Only include localhost URLs in development or when explicitly configured const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? 'http://localhost:3002' : '') const socketWsUrl = socketUrl ? socketUrl.replace('http://', 'ws://').replace('https://', 'wss://') @@ -172,42 +212,24 @@ export function generateRuntimeCSP(): string { const privacyDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_PRIVACY_URL')) const termsDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_TERMS_URL')) - const allDynamicDomains = [ - ...brandLogoDomains, - ...brandFaviconDomains, - ...privacyDomains, - ...termsDomains, - ] - const uniqueDynamicDomains = Array.from(new Set(allDynamicDomains)) - const dynamicDomainsStr = uniqueDynamicDomains.join(' ') - const brandLogoDomain = brandLogoDomains[0] || '' - const brandFaviconDomain = brandFaviconDomains[0] || '' - const reactGrabScript = isReactGrabEnabled ? 'https://unpkg.com' : '' - const gtmScript = isHosted - ? 'https://www.googletagmanager.com https://www.google-analytics.com' - : '' - const gtmConnect = isHosted - ? 'https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com' - : '' - const gtmImg = isHosted ? 'https://www.googletagmanager.com https://www.google-analytics.com' : '' - const gtmFrame = isHosted ? 'https://www.googletagmanager.com' : '' + const runtimeDirectives: CSPDirectives = { + ...buildTimeCSPDirectives, + + 'img-src': [...STATIC_IMG_SRC, ...brandLogoDomains, ...brandFaviconDomains], + + 'connect-src': [ + ...STATIC_CONNECT_SRC, + appUrl, + ollamaUrl, + socketUrl, + socketWsUrl, + ...brandLogoDomains, + ...privacyDomains, + ...termsDomains, + ], + } - return ` - default-src 'self'; - script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://assets.onedollarstats.com https://challenges.cloudflare.com ${reactGrabScript} ${gtmScript}; - style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; - img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.s3.amazonaws.com https://s3.amazonaws.com https://*.amazonaws.com https://*.blob.core.windows.net https://github.com/* https://collector.onedollarstats.com ${gtmImg} ${brandLogoDomain} ${brandFaviconDomain}; - media-src 'self' blob:; - font-src 'self' https://fonts.gstatic.com; - connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://api.browser-use.com https://api.elevenlabs.io wss://api.elevenlabs.io https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.atlassian.com https://*.supabase.co https://challenges.cloudflare.com https://collector.onedollarstats.com ${gtmConnect} ${dynamicDomainsStr}; - frame-src 'self' https://challenges.cloudflare.com https://drive.google.com https://docs.google.com https://*.google.com ${gtmFrame}; - frame-ancestors 'self'; - form-action 'self'; - base-uri 'self'; - object-src 'none'; - ` - .replace(/\s{2,}/g, ' ') - .trim() + return buildCSPString(runtimeDirectives) } /** diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index bf7e51ce5d..2ea6b0383c 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -338,10 +338,10 @@ const nextConfig: NextConfig = { ], }, // Apply security headers to routes not handled by middleware runtime CSP - // Middleware handles: /, /workspace/* + // Middleware handles: /, /login, /signup, /workspace/* // Exclude chat and form routes which have their own permissive embed headers { - source: '/((?!workspace|chat|form).*)', + source: '/((?!workspace|chat|form|login|signup|$).*)', headers: [ { key: 'X-Content-Type-Options', diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index d86d963ed3..6c6a019c67 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -154,6 +154,8 @@ export async function proxy(request: NextRequest) { } 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 track(request, response) } @@ -176,7 +178,11 @@ export async function proxy(request: NextRequest) { if (!hasActiveSession) { return track(request, NextResponse.redirect(new URL('/login', request.url))) } - return track(request, 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 track(request, response) } const invitationRedirect = handleInvitationRedirects(request, hasActiveSession) @@ -191,8 +197,10 @@ export async function proxy(request: NextRequest) { const response = NextResponse.next() response.headers.set('Vary', 'User-Agent') - if (url.pathname.startsWith('/workspace') || url.pathname === '/') { + 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') } return track(request, response)