From 842aa2c254bd4708adfede9c835256180c4e4bf5 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 15 Apr 2026 10:04:53 -0700 Subject: [PATCH 1/7] fix(csp): add missing analytics domains, remove unsafe-eval, fix workspace CSP gap (#4179) --- apps/sim/lib/core/security/csp.ts | 224 ++++++++++++++++-------------- apps/sim/next.config.ts | 4 +- apps/sim/proxy.ts | 12 +- 3 files changed, 135 insertions(+), 105 deletions(-) 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) From 0b36c8bcb6e007fb18ef5aa782c5139ca64b8216 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 15 Apr 2026 11:48:39 -0700 Subject: [PATCH 2/7] fix(landing): return 404 for invalid dynamic route slugs (#4182) * v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * fix(landing): return 404 for invalid dynamic route slugs Add `dynamicParams = false` to all landing page dynamic routes so Next.js returns a proper 404 instead of a client-side exception for slugs not in generateStaticParams. Co-Authored-By: Claude Opus 4.6 * fix(home): remove duplicate handleStopGeneration declaration Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Theodore Li Co-authored-by: Claude Opus 4.6 --- apps/sim/app/(landing)/blog/[slug]/page.tsx | 2 ++ apps/sim/app/(landing)/integrations/[slug]/page.tsx | 2 ++ apps/sim/app/(landing)/models/[provider]/[model]/page.tsx | 2 ++ apps/sim/app/(landing)/models/[provider]/page.tsx | 2 ++ 4 files changed, 8 insertions(+) diff --git a/apps/sim/app/(landing)/blog/[slug]/page.tsx b/apps/sim/app/(landing)/blog/[slug]/page.tsx index 2333ddb67a..0130489be5 100644 --- a/apps/sim/app/(landing)/blog/[slug]/page.tsx +++ b/apps/sim/app/(landing)/blog/[slug]/page.tsx @@ -9,6 +9,8 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import { BackLink } from '@/app/(landing)/blog/[slug]/back-link' import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button' +export const dynamicParams = false + export async function generateStaticParams() { const posts = await getAllPostMeta() return posts.map((p) => ({ slug: p.slug })) diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index d48ea6f405..8915a64349 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -20,6 +20,8 @@ const baseUrl = SITE_URL const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) +export const dynamicParams = false + /** * Returns up to `limit` related integration slugs. * diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx index c8ab7d8c42..b88871460b 100644 --- a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx +++ b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx @@ -20,6 +20,8 @@ import { const baseUrl = SITE_URL +export const dynamicParams = false + export async function generateStaticParams() { return ALL_CATALOG_MODELS.map((model) => ({ provider: model.providerSlug, diff --git a/apps/sim/app/(landing)/models/[provider]/page.tsx b/apps/sim/app/(landing)/models/[provider]/page.tsx index ae2acbe273..20ff3a2026 100644 --- a/apps/sim/app/(landing)/models/[provider]/page.tsx +++ b/apps/sim/app/(landing)/models/[provider]/page.tsx @@ -22,6 +22,8 @@ import { const baseUrl = SITE_URL +export const dynamicParams = false + export async function generateStaticParams() { return MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({ provider: provider.slug, From 5274efd8f97b7254755b19d6c6fc8c908bcc3662 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:13:30 -0700 Subject: [PATCH 3/7] improvement(seo): optimize sitemaps, robots.txt, and core web vitals across sim and docs (#4170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(seo): optimize sitemaps and robots.txt across sim and docs - Add missing pages to sim sitemap: blog author pages, academy catalog and course pages - Fix 6x duplicate URL bug in docs sitemap by deduplicating with source.getLanguages() - Convert docs sitemap from route handler to Next.js metadata convention with native hreflang - Add x-default hreflang alternate for docs multi-language pages - Remove changeFrequency and priority fields (Google ignores both) - Fix inaccurate lastModified timestamps — derive from real content dates, omit when unknown - Consolidate 20+ redundant per-bot robots rules into single wildcard entry - Add /form/ and /credential-account/ to sim robots disallow list - Reference image sitemap in sim robots.txt - Remove deprecated host directive from sim robots - Move disallow rules before allow in docs robots for crawler compatibility - Extract hardcoded docs baseUrl to env variable with production fallback * fix(seo): remove homepage new Date(), guard latestModelDate empty array * improvement(seo): consolidate DOCS_BASE_URL, optimize core web vitals Extract hardcoded https://docs.sim.ai into shared DOCS_BASE_URL constant in lib/urls.ts and replace all 20+ instances across layouts, metadata, structured data, LLM manifest, sitemap, and robots files. Remove OneDollarStats analytics script and tighten CSP for improved core web vitals. * fix: removed onedollarstats from bun lock * fix(seo): guard per-provider Math.max, consolidate docs robots to single wildcard --- apps/docs/app/[lang]/[[...slug]]/page.tsx | 3 +- apps/docs/app/[lang]/layout.tsx | 9 +- apps/docs/app/layout.tsx | 25 ++-- apps/docs/app/llms.txt/route.ts | 3 +- apps/docs/app/robots.txt/route.ts | 73 +--------- apps/docs/app/sitemap.ts | 42 ++++++ apps/docs/app/sitemap.xml/route.ts | 62 -------- apps/docs/components/structured-data.tsx | 4 +- apps/docs/lib/urls.ts | 1 + apps/sim/app/layout.tsx | 6 - apps/sim/app/robots.ts | 136 ++---------------- apps/sim/app/sitemap.ts | 127 +++++++++------- .../components/analytics/onedollarstats.tsx | 26 ---- apps/sim/lib/core/config/env.ts | 1 - apps/sim/lib/core/security/csp.ts | 3 - apps/sim/package.json | 1 - bun.lock | 3 - 17 files changed, 159 insertions(+), 366 deletions(-) create mode 100644 apps/docs/app/sitemap.ts delete mode 100644 apps/docs/app/sitemap.xml/route.ts create mode 100644 apps/docs/lib/urls.ts delete mode 100644 apps/sim/components/analytics/onedollarstats.tsx diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx index d01cd5d359..8bf0c5fd80 100644 --- a/apps/docs/app/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -17,9 +17,10 @@ import { ResponseSection } from '@/components/ui/response-section' import { i18n } from '@/lib/i18n' import { getApiSpecContent, openapi } from '@/lib/openapi' import { type PageData, source } from '@/lib/source' +import { DOCS_BASE_URL } from '@/lib/urls' const SUPPORTED_LANGUAGES: Set = new Set(i18n.languages) -const BASE_URL = 'https://docs.sim.ai' +const BASE_URL = DOCS_BASE_URL const OG_LOCALE_MAP: Record = { en: 'en_US', diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index c500f440cb..4f32bf2076 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -3,7 +3,6 @@ import { defineI18nUI } from 'fumadocs-ui/i18n' import { DocsLayout } from 'fumadocs-ui/layouts/docs' import { RootProvider } from 'fumadocs-ui/provider/next' import { Geist_Mono, Inter } from 'next/font/google' -import Script from 'next/script' import { SidebarFolder, SidebarItem, @@ -13,6 +12,7 @@ import { Navbar } from '@/components/navbar/navbar' import { SimLogoFull } from '@/components/ui/sim-logo' import { i18n } from '@/lib/i18n' import { source } from '@/lib/source' +import { DOCS_BASE_URL } from '@/lib/urls' import '../global.css' const inter = Inter({ @@ -67,14 +67,14 @@ export default async function Layout({ children, params }: LayoutProps) { name: 'Sim Documentation', description: 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', - url: 'https://docs.sim.ai', + url: DOCS_BASE_URL, publisher: { '@type': 'Organization', name: 'Sim', url: 'https://sim.ai', logo: { '@type': 'ImageObject', - url: 'https://docs.sim.ai/static/logo.png', + url: `${DOCS_BASE_URL}/static/logo.png`, }, }, inLanguage: lang, @@ -82,7 +82,7 @@ export default async function Layout({ children, params }: LayoutProps) { '@type': 'SearchAction', target: { '@type': 'EntryPoint', - urlTemplate: 'https://docs.sim.ai/api/search?q={search_term_string}', + urlTemplate: `${DOCS_BASE_URL}/api/search?q={search_term_string}`, }, 'query-input': 'required name=search_term_string', }, @@ -101,7 +101,6 @@ export default async function Layout({ children, params }: LayoutProps) { /> -