Files
simstudioai--sim/apps/sim/lib/core/security/csp.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

286 lines
8.7 KiB
TypeScript

import { env, getEnv } from '../config/env'
import { isDev, isHosted, isReactGrabEnabled } from '../config/env-flags'
/**
* Content Security Policy (CSP) configuration builder
*
* NOTE: This file is loaded by next.config.ts at build time, before @/ path
* aliases are resolved. Do NOT import from ../utils/urls (which uses @/ imports).
* Keep all URL constants local to this file.
*/
const DEFAULT_SOCKET_URL = 'http://localhost:3002'
const DEFAULT_OLLAMA_URL = 'http://localhost:11434'
function toWebSocketUrl(httpUrl: string): string {
return httpUrl.replace('http://', 'ws://').replace('https://', 'wss://')
}
function getHostnameFromUrl(url: string | undefined): string[] {
if (!url) return []
try {
return [`https://${new URL(url).hostname}`]
} catch {
return []
}
}
export interface CSPDirectives {
'default-src'?: string[]
'script-src'?: string[]
'style-src'?: string[]
'img-src'?: string[]
'media-src'?: string[]
'font-src'?: string[]
'connect-src'?: string[]
'worker-src'?: string[]
'frame-src'?: string[]
'frame-ancestors'?: string[]
'form-action'?: string[]
'base-uri'?: string[]
'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'",
...(isDev ? ["'unsafe-eval'"] : []),
'https://*.google.com',
'https://apis.google.com',
'https://challenges.cloudflare.com',
// Cal.com booking embed (landing /demo) — embed.js is served from app.cal.com
'https://app.cal.com',
...(isReactGrabEnabled ? ['https://unpkg.com'] : []),
...(isHosted
? [
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://analytics.ahrefs.com',
// HubSpot tracking (landing pages) — loader plus the
// analytics/form-tracking/banner scripts it injects as <script> tags
'https://*.hs-scripts.com',
'https://*.hs-analytics.net',
'https://*.hscollectedforms.net',
'https://*.hs-banner.com',
]
: []),
] as const
const STATIC_IMG_SRC = ["'self'", 'data:', 'blob:', 'https:'] 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',
// Cal.com booking embed (landing /demo) — embed XHR/availability calls
'https://app.cal.com',
'https://cal.com',
...(isReactGrabEnabled ? ['https://www.react-grab.com'] : []),
...(isDev ? ['ws://localhost:4722'] : []),
...(isHosted
? [
'https://www.googletagmanager.com',
'https://*.google-analytics.com',
'https://*.analytics.google.com',
'https://analytics.google.com',
'https://www.google.com',
'https://analytics.ahrefs.com',
'https://*.g.doubleclick.net',
// HubSpot tracking — form-tracking API (hscollectedforms.js).
// The visitor beacon itself is an image pixel (img-src, already
// permitted below), not a connect-src request.
'https://*.hscollectedforms.net',
]
: []),
] as const
const STATIC_FRAME_SRC = [
"'self'",
'blob:',
'https://challenges.cloudflare.com',
// Cal.com booking embed (landing /demo) — the booking iframe
'https://app.cal.com',
'https://cal.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': [...STATIC_SCRIPT_SRC],
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
'img-src': [...STATIC_IMG_SRC],
'media-src': ["'self'", 'blob:'],
'worker-src': ["'self'", 'blob:'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'connect-src': [
...STATIC_CONNECT_SRC,
env.NEXT_PUBLIC_APP_URL || '',
...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? [DEFAULT_OLLAMA_URL] : []),
...(env.NEXT_PUBLIC_SOCKET_URL
? [env.NEXT_PUBLIC_SOCKET_URL, toWebSocketUrl(env.NEXT_PUBLIC_SOCKET_URL)]
: isDev
? [DEFAULT_SOCKET_URL, toWebSocketUrl(DEFAULT_SOCKET_URL)]
: []),
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL),
...getHostnameFromUrl(env.NEXT_PUBLIC_TERMS_URL),
],
'frame-src': [...STATIC_FRAME_SRC],
'frame-ancestors': ["'self'"],
'form-action': ["'self'"],
'base-uri': ["'self'"],
'object-src': ["'none'"],
}
/**
* Build CSP string from directives object
*/
export function buildCSPString(directives: CSPDirectives): string {
return Object.entries(directives)
.map(([directive, sources]) => {
if (!sources || sources.length === 0) return ''
const validSources = sources.filter((source: string) => source && source.trim() !== '')
if (validSources.length === 0) return ''
return `${directive} ${validSources.join(' ')}`
})
.filter(Boolean)
.join('; ')
}
/**
* 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') || ''
const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? DEFAULT_SOCKET_URL : '')
const socketWsUrl = socketUrl ? toWebSocketUrl(socketUrl) : ''
const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? DEFAULT_OLLAMA_URL : '')
const brandLogoDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL'))
const privacyDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_PRIVACY_URL'))
const termsDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_TERMS_URL'))
const runtimeDirectives: CSPDirectives = {
...buildTimeCSPDirectives,
'img-src': [...STATIC_IMG_SRC],
'connect-src': [
...STATIC_CONNECT_SRC,
appUrl,
ollamaUrl,
socketUrl,
socketWsUrl,
...brandLogoDomains,
...privacyDomains,
...termsDomains,
],
}
return buildCSPString(runtimeDirectives)
}
/**
* Get the main CSP policy string (build-time)
*/
export function getMainCSPPolicy(): string {
return buildCSPString(buildTimeCSPDirectives)
}
/**
* Permissive CSP for workflow execution endpoints
*/
export function getWorkflowExecutionCSPPolicy(): string {
return "default-src * 'unsafe-inline' 'unsafe-eval'; connect-src *;"
}
/**
* CSP for embeddable chat pages.
* Extends the shared embed policy with Microsoft Office.js sources so the
* chat page can serve as an Office (Excel/Word/Outlook) add-in surface
* when loaded with `?embed=office`.
*/
export function getChatEmbedCSPPolicy(): string {
return buildCSPString({
...buildTimeCSPDirectives,
'script-src': [...STATIC_SCRIPT_SRC, 'https://appsforoffice.microsoft.com'],
'connect-src': [
...(buildTimeCSPDirectives['connect-src'] ?? []),
'https://appsforoffice.microsoft.com',
],
'frame-ancestors': ['*'],
})
}
/**
* Add a source to a specific directive (modifies build-time directives)
*/
export function addCSPSource(directive: keyof CSPDirectives, source: string): void {
if (!buildTimeCSPDirectives[directive]) {
buildTimeCSPDirectives[directive] = []
}
if (!buildTimeCSPDirectives[directive]!.includes(source)) {
buildTimeCSPDirectives[directive]!.push(source)
}
}
/**
* Remove a source from a specific directive (modifies build-time directives)
*/
export function removeCSPSource(directive: keyof CSPDirectives, source: string): void {
if (buildTimeCSPDirectives[directive]) {
buildTimeCSPDirectives[directive] = buildTimeCSPDirectives[directive]!.filter(
(s: string) => s !== source
)
}
}