diff --git a/src/chat-sidebar.tsx b/src/chat-sidebar.tsx index dd4296ab..24626aee 100644 --- a/src/chat-sidebar.tsx +++ b/src/chat-sidebar.tsx @@ -74,6 +74,7 @@ import { isDarkTheme, writeTextToClipboard } from './utils'; +import { injectHtmlFrameCsp } from './html-frame-csp'; import { CheckBoxItem } from './components/checkbox'; import { mcpServerSettingsToEnabledState } from './components/mcp-util'; import claudeSvgStr from '../style/icons/claude.svg'; @@ -499,7 +500,10 @@ const answeredForms = new Map(); function ChatResponseHTMLFrame(props: any) { const iframSrc = useMemo( - () => URL.createObjectURL(new Blob([props.source], { type: 'text/html' })), + () => + URL.createObjectURL( + new Blob([injectHtmlFrameCsp(props.source)], { type: 'text/html' }) + ), [] ); return ( diff --git a/src/html-frame-csp.ts b/src/html-frame-csp.ts new file mode 100644 index 00000000..42b23919 --- /dev/null +++ b/src/html-frame-csp.ts @@ -0,0 +1,71 @@ +// Copyright (c) Mehmet Bektas + +/** + * Content-Security-Policy injected into every HTMLFrame blob document. + * + * The iframe already runs with sandbox="allow-scripts" (no allow-same-origin), + * so cookies and parent DOM are unreachable. But scripts in a null-origin + * context can still fetch() to any URL the user's browser can reach: + * cluster intranet, 169.254.169.254 cloud metadata, the Jupyter server + * itself. Inline scripts/styles stay enabled because most LLM/tool HTML + * output (matplotlib, plotly, custom dashboards) is self-contained inline; + * external CDN loads and any network egress are blocked. img/font are + * allowed only as data: URIs so inline visualizations still render. + */ +export const HTML_FRAME_CSP = [ + "default-src 'none'", + "script-src 'unsafe-inline'", + "style-src 'unsafe-inline'", + 'img-src data:', + 'font-src data:', + "connect-src 'none'", + "base-uri 'none'", + "form-action 'none'", + "frame-src 'none'", + "child-src 'none'", + "worker-src 'none'", + "manifest-src 'none'", + "media-src 'none'", + "object-src 'none'" +].join('; '); + +const CSP_META_TAG = ``; + +// `` or any other doctype declaration MUST be the very +// first thing in the document or the parser drops into quirks mode, +// which breaks CSS that matplotlib/plotly emit (and disables a handful +// of CSP enforcement details in older WebKit). Detect a leading doctype +// (allowing for an optional BOM and leading whitespace) and slot the +// meta right after it instead of before it. +const DOCTYPE_RE = /^(?:\ufeff)?\s*]*>/i; + +/** + * Prepend the CSP `` tag to untrusted HTML so the resulting blob + * document boots under policy. + * + * Almost-always prepend rather than trying to locate ``: an + * HTML-naive regex can be fooled by `` inside a comment, + * `