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>
This commit is contained in:
Waleed
2026-03-17 17:12:34 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 75a3e2c3a8
commit c9f082da1a
3 changed files with 41 additions and 12 deletions
+17 -4
View File
@@ -202,15 +202,28 @@ export function getWorkflowExecutionCSPPolicy(): string {
}
/**
* CSP for embeddable form pages
* Shared CSP for embeddable pages (chat, forms)
* Allows embedding in iframes from any origin while maintaining other security policies
*/
export function getFormEmbedCSPPolicy(): string {
const basePolicy = buildCSPString({
function getEmbedCSPPolicy(): string {
return buildCSPString({
...buildTimeCSPDirectives,
'frame-ancestors': ['*'],
})
return basePolicy
}
/**
* CSP for embeddable chat pages
*/
export function getChatEmbedCSPPolicy(): string {
return getEmbedCSPPolicy()
}
/**
* CSP for embeddable form pages
*/
export function getFormEmbedCSPPolicy(): string {
return getEmbedCSPPolicy()
}
/**
+22 -3
View File
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next'
import { env, getEnv, isTruthy } from './lib/core/config/env'
import { isDev } from './lib/core/config/feature-flags'
import {
getChatEmbedCSPPolicy,
getFormEmbedCSPPolicy,
getMainCSPPolicy,
getWorkflowExecutionCSPPolicy,
@@ -255,6 +256,24 @@ const nextConfig: NextConfig = {
},
],
},
// Chat pages - allow iframe embedding from any origin
{
source: '/chat/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
// No X-Frame-Options to allow iframe embedding
{
key: 'Content-Security-Policy',
value: getChatEmbedCSPPolicy(),
},
// Permissive CORS for chat requests from embedded chats
{ key: 'Cross-Origin-Embedder-Policy', value: 'unsafe-none' },
{ key: 'Cross-Origin-Opener-Policy', value: 'unsafe-none' },
],
},
// Form pages - allow iframe embedding from any origin
{
source: '/form/:path*',
@@ -284,10 +303,10 @@ const nextConfig: NextConfig = {
],
},
// Apply security headers to routes not handled by middleware runtime CSP
// Middleware handles: /, /workspace/*, /chat/*
// Exclude form routes which have their own permissive headers
// Middleware handles: /, /workspace/*
// Exclude chat and form routes which have their own permissive embed headers
{
source: '/((?!workspace|chat$|form).*)',
source: '/((?!workspace|chat|form).*)',
headers: [
{
key: 'X-Content-Type-Options',
+2 -5
View File
@@ -155,6 +155,7 @@ export async function proxy(request: NextRequest) {
return response
}
// Chat pages are publicly accessible embeds — CSP is set in next.config.ts headers
if (url.pathname.startsWith('/chat/')) {
return NextResponse.next()
}
@@ -188,11 +189,7 @@ export async function proxy(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('Vary', 'User-Agent')
if (
url.pathname.startsWith('/workspace') ||
url.pathname.startsWith('/chat') ||
url.pathname === '/'
) {
if (url.pathname.startsWith('/workspace') || url.pathname === '/') {
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
}