Skip to content

Commit c9f082d

Browse files
waleedlatif1claude
andauthored
feat(csp): allow chat UI to be embedded in iframes (#3643)
* feat(csp): allow chat UI to be embedded in iframes Mirror the existing form embed CSP pattern for chat pages: add getChatEmbedCSPPolicy() with frame-ancestors *, configure /chat/:path* headers in next.config.ts without X-Frame-Options, and early-return in proxy.ts so chat routes skip the strict runtime CSP. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(csp): extract shared getEmbedCSPPolicy helper Deduplicate getChatEmbedCSPPolicy and getFormEmbedCSPPolicy into a shared private helper to prevent future divergence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 75a3e2c commit c9f082d

File tree

3 files changed

+41
-12
lines changed

3 files changed

+41
-12
lines changed

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,28 @@ export function getWorkflowExecutionCSPPolicy(): string {
202202
}
203203

204204
/**
205-
* CSP for embeddable form pages
205+
* Shared CSP for embeddable pages (chat, forms)
206206
* Allows embedding in iframes from any origin while maintaining other security policies
207207
*/
208-
export function getFormEmbedCSPPolicy(): string {
209-
const basePolicy = buildCSPString({
208+
function getEmbedCSPPolicy(): string {
209+
return buildCSPString({
210210
...buildTimeCSPDirectives,
211211
'frame-ancestors': ['*'],
212212
})
213-
return basePolicy
213+
}
214+
215+
/**
216+
* CSP for embeddable chat pages
217+
*/
218+
export function getChatEmbedCSPPolicy(): string {
219+
return getEmbedCSPPolicy()
220+
}
221+
222+
/**
223+
* CSP for embeddable form pages
224+
*/
225+
export function getFormEmbedCSPPolicy(): string {
226+
return getEmbedCSPPolicy()
214227
}
215228

216229
/**

apps/sim/next.config.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next'
22
import { env, getEnv, isTruthy } from './lib/core/config/env'
33
import { isDev } from './lib/core/config/feature-flags'
44
import {
5+
getChatEmbedCSPPolicy,
56
getFormEmbedCSPPolicy,
67
getMainCSPPolicy,
78
getWorkflowExecutionCSPPolicy,
@@ -255,6 +256,24 @@ const nextConfig: NextConfig = {
255256
},
256257
],
257258
},
259+
// Chat pages - allow iframe embedding from any origin
260+
{
261+
source: '/chat/:path*',
262+
headers: [
263+
{
264+
key: 'X-Content-Type-Options',
265+
value: 'nosniff',
266+
},
267+
// No X-Frame-Options to allow iframe embedding
268+
{
269+
key: 'Content-Security-Policy',
270+
value: getChatEmbedCSPPolicy(),
271+
},
272+
// Permissive CORS for chat requests from embedded chats
273+
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
274+
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
275+
],
276+
},
258277
// Form pages - allow iframe embedding from any origin
259278
{
260279
source: '/form/:path*',
@@ -284,10 +303,10 @@ const nextConfig: NextConfig = {
284303
],
285304
},
286305
// Apply security headers to routes not handled by middleware runtime CSP
287-
// Middleware handles: /, /workspace/*, /chat/*
288-
// Exclude form routes which have their own permissive headers
306+
// Middleware handles: /, /workspace/*
307+
// Exclude chat and form routes which have their own permissive embed headers
289308
{
290-
source: '/((?!workspace|chat$|form).*)',
309+
source: '/((?!workspace|chat|form).*)',
291310
headers: [
292311
{
293312
key: 'X-Content-Type-Options',

apps/sim/proxy.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export async function proxy(request: NextRequest) {
155155
return response
156156
}
157157

158+
// Chat pages are publicly accessible embeds — CSP is set in next.config.ts headers
158159
if (url.pathname.startsWith('/chat/')) {
159160
return NextResponse.next()
160161
}
@@ -188,11 +189,7 @@ export async function proxy(request: NextRequest) {
188189
const response = NextResponse.next()
189190
response.headers.set('Vary', 'User-Agent')
190191

191-
if (
192-
url.pathname.startsWith('/workspace') ||
193-
url.pathname.startsWith('/chat') ||
194-
url.pathname === '/'
195-
) {
192+
if (url.pathname.startsWith('/workspace') || url.pathname === '/') {
196193
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
197194
}
198195

0 commit comments

Comments
 (0)