Skip to content

Commit 842aa2c

Browse files
authored
fix(csp): add missing analytics domains, remove unsafe-eval, fix workspace CSP gap (#4179)
1 parent ff71a07 commit 842aa2c

File tree

3 files changed

+135
-105
lines changed

3 files changed

+135
-105
lines changed

apps/sim/lib/core/security/csp.ts

Lines changed: 123 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -29,38 +29,112 @@ export interface CSPDirectives {
2929
'object-src'?: string[]
3030
}
3131

32+
/**
33+
* Static CSP sources shared between build-time and runtime.
34+
* Add new domains here — both paths pick them up automatically.
35+
*/
36+
const STATIC_SCRIPT_SRC = [
37+
"'self'",
38+
"'unsafe-inline'",
39+
'https://*.google.com',
40+
'https://apis.google.com',
41+
'https://assets.onedollarstats.com',
42+
'https://challenges.cloudflare.com',
43+
...(isReactGrabEnabled ? ['https://unpkg.com'] : []),
44+
...(isHosted
45+
? [
46+
'https://www.googletagmanager.com',
47+
'https://www.google-analytics.com',
48+
'https://analytics.ahrefs.com',
49+
]
50+
: []),
51+
] as const
52+
53+
const STATIC_IMG_SRC = [
54+
"'self'",
55+
'data:',
56+
'blob:',
57+
'https://*.googleusercontent.com',
58+
'https://*.google.com',
59+
'https://*.atlassian.com',
60+
'https://cdn.discordapp.com',
61+
'https://*.githubusercontent.com',
62+
'https://*.s3.amazonaws.com',
63+
'https://s3.amazonaws.com',
64+
'https://*.amazonaws.com',
65+
'https://*.blob.core.windows.net',
66+
'https://github.com/*',
67+
'https://collector.onedollarstats.com',
68+
'https://cursor.com',
69+
...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []),
70+
] as const
71+
72+
const STATIC_CONNECT_SRC = [
73+
"'self'",
74+
'https://api.browser-use.com',
75+
'https://api.elevenlabs.io',
76+
'wss://api.elevenlabs.io',
77+
'https://api.exa.ai',
78+
'https://api.firecrawl.dev',
79+
'https://*.googleapis.com',
80+
'https://*.amazonaws.com',
81+
'https://*.s3.amazonaws.com',
82+
'https://*.blob.core.windows.net',
83+
'https://*.atlassian.com',
84+
'https://*.supabase.co',
85+
'https://api.github.com',
86+
'https://github.com/*',
87+
'https://challenges.cloudflare.com',
88+
'https://collector.onedollarstats.com',
89+
...(isHosted
90+
? [
91+
'https://www.googletagmanager.com',
92+
'https://*.google-analytics.com',
93+
'https://*.analytics.google.com',
94+
'https://analytics.google.com',
95+
'https://www.google.com',
96+
]
97+
: []),
98+
] as const
99+
100+
const STATIC_FRAME_SRC = [
101+
"'self'",
102+
'https://challenges.cloudflare.com',
103+
'https://drive.google.com',
104+
'https://docs.google.com',
105+
'https://*.google.com',
106+
'https://www.youtube.com',
107+
'https://player.vimeo.com',
108+
'https://www.dailymotion.com',
109+
'https://player.twitch.tv',
110+
'https://clips.twitch.tv',
111+
'https://streamable.com',
112+
'https://fast.wistia.net',
113+
'https://www.tiktok.com',
114+
'https://w.soundcloud.com',
115+
'https://open.spotify.com',
116+
'https://embed.music.apple.com',
117+
'https://www.loom.com',
118+
'https://www.facebook.com',
119+
'https://www.instagram.com',
120+
'https://platform.twitter.com',
121+
'https://rumble.com',
122+
'https://play.vidyard.com',
123+
'https://iframe.cloudflarestream.com',
124+
'https://www.mixcloud.com',
125+
'https://tenor.com',
126+
'https://giphy.com',
127+
...(isHosted ? ['https://www.googletagmanager.com'] : []),
128+
] as const
129+
32130
// Build-time CSP directives (for next.config.ts)
33131
export const buildTimeCSPDirectives: CSPDirectives = {
34132
'default-src': ["'self'"],
35-
36-
'script-src': [
37-
"'self'",
38-
"'unsafe-inline'",
39-
"'unsafe-eval'",
40-
'https://*.google.com',
41-
'https://apis.google.com',
42-
'https://assets.onedollarstats.com',
43-
'https://challenges.cloudflare.com',
44-
...(isReactGrabEnabled ? ['https://unpkg.com'] : []),
45-
...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []),
46-
],
47-
133+
'script-src': [...STATIC_SCRIPT_SRC],
48134
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
49135

50136
'img-src': [
51-
"'self'",
52-
'data:',
53-
'blob:',
54-
'https://*.googleusercontent.com',
55-
'https://*.google.com',
56-
'https://*.atlassian.com',
57-
'https://cdn.discordapp.com',
58-
'https://*.githubusercontent.com',
59-
'https://*.s3.amazonaws.com',
60-
'https://s3.amazonaws.com',
61-
'https://github.com/*',
62-
'https://collector.onedollarstats.com',
63-
...(isHosted ? ['https://www.googletagmanager.com', 'https://www.google-analytics.com'] : []),
137+
...STATIC_IMG_SRC,
64138
...(env.S3_BUCKET_NAME && env.AWS_REGION
65139
? [`https://${env.S3_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`]
66140
: []),
@@ -70,21 +144,16 @@ export const buildTimeCSPDirectives: CSPDirectives = {
70144
...(env.S3_CHAT_BUCKET_NAME && env.AWS_REGION
71145
? [`https://${env.S3_CHAT_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`]
72146
: []),
73-
'https://*.amazonaws.com',
74-
'https://*.blob.core.windows.net',
75-
'https://github.com/*',
76147
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
77148
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_FAVICON_URL),
78149
],
79150

80151
'media-src': ["'self'", 'blob:'],
81-
82152
'font-src': ["'self'", 'https://fonts.gstatic.com'],
83153

84154
'connect-src': [
85-
"'self'",
155+
...STATIC_CONNECT_SRC,
86156
env.NEXT_PUBLIC_APP_URL || '',
87-
// Only include localhost fallbacks in development mode
88157
...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? ['http://localhost:11434'] : []),
89158
...(env.NEXT_PUBLIC_SOCKET_URL
90159
? [
@@ -94,42 +163,12 @@ export const buildTimeCSPDirectives: CSPDirectives = {
94163
: isDev
95164
? ['http://localhost:3002', 'ws://localhost:3002']
96165
: []),
97-
'https://api.browser-use.com',
98-
'https://api.elevenlabs.io',
99-
'wss://api.elevenlabs.io',
100-
'https://api.exa.ai',
101-
'https://api.firecrawl.dev',
102-
'https://*.googleapis.com',
103-
'https://*.amazonaws.com',
104-
'https://*.s3.amazonaws.com',
105-
'https://*.blob.core.windows.net',
106-
'https://*.atlassian.com',
107-
'https://*.supabase.co',
108-
'https://api.github.com',
109-
'https://github.com/*',
110-
'https://challenges.cloudflare.com',
111-
'https://collector.onedollarstats.com',
112-
...(isHosted
113-
? [
114-
'https://www.googletagmanager.com',
115-
'https://*.google-analytics.com',
116-
'https://*.analytics.google.com',
117-
]
118-
: []),
119166
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
120167
...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL),
121168
...getHostnameFromUrl(env.NEXT_PUBLIC_TERMS_URL),
122169
],
123170

124-
'frame-src': [
125-
"'self'",
126-
'https://challenges.cloudflare.com',
127-
'https://drive.google.com',
128-
'https://docs.google.com',
129-
'https://*.google.com',
130-
...(isHosted ? ['https://www.googletagmanager.com'] : []),
131-
],
132-
171+
'frame-src': [...STATIC_FRAME_SRC],
133172
'frame-ancestors': ["'self'"],
134173
'form-action': ["'self'"],
135174
'base-uri': ["'self'"],
@@ -152,13 +191,14 @@ export function buildCSPString(directives: CSPDirectives): string {
152191
}
153192

154193
/**
155-
* Generate runtime CSP header with dynamic environment variables (safer approach)
156-
* This maintains compatibility with existing inline scripts while fixing Docker env var issues
194+
* Generate runtime CSP header with dynamic environment variables.
195+
* Composes from the same STATIC_* constants as buildTimeCSPDirectives,
196+
* but resolves env vars at request time via getEnv() to fix Docker
197+
* deployments where build-time values may be stale placeholders.
157198
*/
158199
export function generateRuntimeCSP(): string {
159200
const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || ''
160201

161-
// Only include localhost URLs in development or when explicitly configured
162202
const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? 'http://localhost:3002' : '')
163203
const socketWsUrl = socketUrl
164204
? socketUrl.replace('http://', 'ws://').replace('https://', 'wss://')
@@ -172,42 +212,24 @@ export function generateRuntimeCSP(): string {
172212
const privacyDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_PRIVACY_URL'))
173213
const termsDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_TERMS_URL'))
174214

175-
const allDynamicDomains = [
176-
...brandLogoDomains,
177-
...brandFaviconDomains,
178-
...privacyDomains,
179-
...termsDomains,
180-
]
181-
const uniqueDynamicDomains = Array.from(new Set(allDynamicDomains))
182-
const dynamicDomainsStr = uniqueDynamicDomains.join(' ')
183-
const brandLogoDomain = brandLogoDomains[0] || ''
184-
const brandFaviconDomain = brandFaviconDomains[0] || ''
185-
const reactGrabScript = isReactGrabEnabled ? 'https://unpkg.com' : ''
186-
const gtmScript = isHosted
187-
? 'https://www.googletagmanager.com https://www.google-analytics.com'
188-
: ''
189-
const gtmConnect = isHosted
190-
? 'https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com'
191-
: ''
192-
const gtmImg = isHosted ? 'https://www.googletagmanager.com https://www.google-analytics.com' : ''
193-
const gtmFrame = isHosted ? 'https://www.googletagmanager.com' : ''
215+
const runtimeDirectives: CSPDirectives = {
216+
...buildTimeCSPDirectives,
217+
218+
'img-src': [...STATIC_IMG_SRC, ...brandLogoDomains, ...brandFaviconDomains],
219+
220+
'connect-src': [
221+
...STATIC_CONNECT_SRC,
222+
appUrl,
223+
ollamaUrl,
224+
socketUrl,
225+
socketWsUrl,
226+
...brandLogoDomains,
227+
...privacyDomains,
228+
...termsDomains,
229+
],
230+
}
194231

195-
return `
196-
default-src 'self';
197-
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://assets.onedollarstats.com https://challenges.cloudflare.com ${reactGrabScript} ${gtmScript};
198-
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
199-
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};
200-
media-src 'self' blob:;
201-
font-src 'self' https://fonts.gstatic.com;
202-
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};
203-
frame-src 'self' https://challenges.cloudflare.com https://drive.google.com https://docs.google.com https://*.google.com ${gtmFrame};
204-
frame-ancestors 'self';
205-
form-action 'self';
206-
base-uri 'self';
207-
object-src 'none';
208-
`
209-
.replace(/\s{2,}/g, ' ')
210-
.trim()
232+
return buildCSPString(runtimeDirectives)
211233
}
212234

213235
/**

apps/sim/next.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,10 @@ const nextConfig: NextConfig = {
338338
],
339339
},
340340
// Apply security headers to routes not handled by middleware runtime CSP
341-
// Middleware handles: /, /workspace/*
341+
// Middleware handles: /, /login, /signup, /workspace/*
342342
// Exclude chat and form routes which have their own permissive embed headers
343343
{
344-
source: '/((?!workspace|chat|form).*)',
344+
source: '/((?!workspace|chat|form|login|signup|$).*)',
345345
headers: [
346346
{
347347
key: 'X-Content-Type-Options',

apps/sim/proxy.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ export async function proxy(request: NextRequest) {
154154
}
155155
const response = NextResponse.next()
156156
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
157+
response.headers.set('X-Content-Type-Options', 'nosniff')
158+
response.headers.set('X-Frame-Options', 'SAMEORIGIN')
157159
return track(request, response)
158160
}
159161

@@ -176,7 +178,11 @@ export async function proxy(request: NextRequest) {
176178
if (!hasActiveSession) {
177179
return track(request, NextResponse.redirect(new URL('/login', request.url)))
178180
}
179-
return track(request, NextResponse.next())
181+
const response = NextResponse.next()
182+
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
183+
response.headers.set('X-Content-Type-Options', 'nosniff')
184+
response.headers.set('X-Frame-Options', 'SAMEORIGIN')
185+
return track(request, response)
180186
}
181187

182188
const invitationRedirect = handleInvitationRedirects(request, hasActiveSession)
@@ -191,8 +197,10 @@ export async function proxy(request: NextRequest) {
191197
const response = NextResponse.next()
192198
response.headers.set('Vary', 'User-Agent')
193199

194-
if (url.pathname.startsWith('/workspace') || url.pathname === '/') {
200+
if (url.pathname === '/') {
195201
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
202+
response.headers.set('X-Content-Type-Options', 'nosniff')
203+
response.headers.set('X-Frame-Options', 'SAMEORIGIN')
196204
}
197205

198206
return track(request, response)

0 commit comments

Comments
 (0)