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:
@@ -1376,6 +1376,7 @@ RESPONSE_WATERMARK = PersistentConfig(
|
||||
os.environ.get('RESPONSE_WATERMARK', ''),
|
||||
)
|
||||
|
||||
IFRAME_CSP = os.environ.get('IFRAME_CSP', '')
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true'
|
||||
|
||||
@@ -460,6 +460,7 @@ from open_webui.config import (
|
||||
OAUTH_PROVIDERS,
|
||||
WEBUI_URL,
|
||||
RESPONSE_WATERMARK,
|
||||
IFRAME_CSP,
|
||||
# Admin
|
||||
ENABLE_ADMIN_CHAT_ACCESS,
|
||||
ENABLE_ADMIN_ANALYTICS,
|
||||
@@ -2444,6 +2445,7 @@ async def get_app_config(request: Request):
|
||||
'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||
'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||
'response_watermark': app.state.config.RESPONSE_WATERMARK,
|
||||
'iframe_csp': IFRAME_CSP,
|
||||
},
|
||||
'license_metadata': app.state.LICENSE_METADATA,
|
||||
**(
|
||||
|
||||
@@ -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;` : ''}`}
|
||||
|
||||
@@ -309,6 +309,7 @@ type Config = {
|
||||
ui?: {
|
||||
pending_user_overlay_title?: string;
|
||||
pending_user_overlay_content?: string;
|
||||
iframe_csp?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Prepend a Content-Security-Policy <meta> tag to HTML.
|
||||
* First CSP meta tag wins per spec, so any existing CSP
|
||||
* in the HTML is effectively overridden.
|
||||
*/
|
||||
export function injectCsp(html: string, csp: string): string {
|
||||
if (!csp) return html;
|
||||
const escaped = csp.replace(/"/g, '"');
|
||||
const tag = `<meta http-equiv="Content-Security-Policy" content="${escaped}">`;
|
||||
const idx = html.indexOf('<head>');
|
||||
return idx !== -1
|
||||
? html.slice(0, idx + 6) + tag + html.slice(idx + 6)
|
||||
: tag + html;
|
||||
}
|
||||
Reference in New Issue
Block a user