feat: add IFRAME_CSP env var for srcdoc iframe content security policy

Adds an IFRAME_CSP environment variable that injects a Content-Security-Policy
<meta> tag into all srcdoc iframes rendering untrusted content:
- Artifacts (LLM-generated HTML previews)
- FullHeightIframe (tool/embed output)
- FilePreview (user-uploaded HTML files)
- CitationModal (RAG document HTML)

Shared utility in src/lib/utils/csp.ts handles injection with HTML-safe
attribute escaping. URL-based iframes (src=) are correctly excluded.

Env-var only — no PersistentConfig, no admin UI, no DB. Set once at deploy
time, requires restart. Empty string (default) means no CSP restriction.
This commit is contained in:
Timothy Jaeryang Baek
2026-05-11 01:56:02 +09:00
parent 1388f4568b
commit 3bba1c2270
8 changed files with 30 additions and 6 deletions
+3 -1
View File
@@ -7,12 +7,14 @@
import {
artifactCode,
chatId,
config,
settings,
showArtifacts,
showControls,
artifactContents
} from '$lib/stores';
import { copyToClipboard, createMessagesList } from '$lib/utils';
import { injectCsp } from '$lib/utils/csp';
import XMark from '../icons/XMark.svelte';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
@@ -242,7 +244,7 @@
<iframe
bind:this={iframeElement}
title="Content"
srcdoc={contents[selectedContentIdx].content}
srcdoc={injectCsp(contents[selectedContentIdx].content, $config?.ui?.iframe_csp ?? '')}
class="w-full border-0 h-full rounded-none"
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
@@ -2,7 +2,8 @@
import { getContext, tick } from 'svelte';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { settings } from '$lib/stores';
import { settings, config } from '$lib/stores';
import { injectCsp } from '$lib/utils/csp';
import { isCodeFile } from '$lib/utils/codeHighlight';
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
import Spinner from '../../common/Spinner.svelte';
@@ -411,7 +412,7 @@
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
{/if}
<iframe
srcdoc={fileContent}
srcdoc={injectCsp(fileContent, $config?.ui?.iframe_csp ?? '')}
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
@@ -4,7 +4,8 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { settings } from '$lib/stores';
import { settings, config } from '$lib/stores';
import { injectCsp } from '$lib/utils/csp';
import XMark from '$lib/components/icons/XMark.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
@@ -218,7 +219,7 @@
false)
? ' allow-same-origin'
: ''}"
srcdoc={document.document}
srcdoc={injectCsp(document.document, $config?.ui?.iframe_csp ?? '')}
title={$i18n.t('Content')}
></iframe>
{:else}
@@ -1,5 +1,7 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte';
import { config } from '$lib/stores';
import { injectCsp } from '$lib/utils/csp';
// Props
export let src: string | null = null; // URL or raw HTML (auto-detected)
@@ -192,7 +194,7 @@ window.Chart = parent.Chart; // Chart previously assigned on parent
{#if iframeDoc}
<iframe
bind:this={iframe}
srcdoc={iframeDoc}
srcdoc={injectCsp(iframeDoc, $config?.ui?.iframe_csp ?? '')}
{title}
class={iframeClassName}
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}