Studio: block tabnabbing from HTML preview popups

Codex P1 on the previous commit: granting both ``allow-popups`` and
``allow-popups-to-escape-sandbox`` while forcing every link through
``<base target="_blank">`` lets a malicious assistant-emitted link
open a regular browser tab that retains ``window.opener``. The
destination page can then call
``window.opener.top.location.href = '...'`` and tabnab the original
Studio tab.

Drop ``allow-popups-to-escape-sandbox``. ``allow-popups`` stays so
the click still produces a tab instead of being silently dropped,
but the popup now INHERITS the iframe sandbox (no
allow-same-origin, no allow-top-navigation), so it cannot reach
back into the host. Trade-off: the opened tab loads with an opaque
origin and some real sites render degraded inside the popup. This
is the deliberate exchange for tabnabbing safety; the chat-message
preview iframe is the primary surface, the popup is a follow-up
link click.

Existing rendering-iframe sandbox test now asserts
allow-popups-to-escape-sandbox is NOT present.
This commit is contained in:
Daniel Han 2026-05-25 09:50:20 +00:00
commit e3c7948874
2 changed files with 20 additions and 13 deletions

View file

@ -26,17 +26,19 @@ describe("HtmlSvgRenderer", () => {
) as HTMLIFrameElement;
expect(iframe.tagName).toBe("IFRAME");
// SECURITY: allow-scripts + allow-modals leave script / alert /
// confirm operative IF the inherited host CSP ever permits inline;
// allow-popups + allow-popups-to-escape-sandbox so a `<base
// target="_blank">` link does not silently no-op; NEVER
// allow-same-origin or allow-top-navigation -- those would let
// the preview read parent.document and exfiltrate session data.
// confirm operative if the inherited host CSP ever permits inline.
// allow-popups lets ``<base target="_blank">`` links open without
// being silently dropped, BUT the popups INHERIT the sandbox
// (allow-popups-to-escape-sandbox is intentionally absent) so a
// tab opened from a malicious assistant link cannot use
// window.opener.top.location.* to tabnab the Studio tab.
// allow-same-origin and allow-top-navigation are NEVER granted.
const sandbox = iframe.getAttribute("sandbox") ?? "";
const sandboxTokens = sandbox.split(/\s+/);
expect(sandboxTokens).toContain("allow-scripts");
expect(sandboxTokens).toContain("allow-modals");
expect(sandboxTokens).toContain("allow-popups");
expect(sandboxTokens).toContain("allow-popups-to-escape-sandbox");
expect(sandboxTokens).not.toContain("allow-popups-to-escape-sandbox");
expect(sandbox).not.toContain("allow-same-origin");
expect(sandbox).not.toContain("allow-top-navigation");
// srcdoc carries the assistant HTML plus a defense-in-depth meta

View file

@ -356,13 +356,18 @@ function HtmlPreview({
// that includes 'unsafe-inline'
// allow-modals -- alert/confirm/prompt are not no-ops
// when scripts do fire
// allow-popups + -- a ``<base target="_blank">`` link
// allow-popups-to- opens a regular browser tab rather
// escape-sandbox than an opaque-origin sandboxed one
// We do NOT grant allow-same-origin or allow-top-navigation, so
// the iframe cannot read parent.document, navigate the host
// page, or escape its origin.
sandbox="allow-scripts allow-modals allow-popups allow-popups-to-escape-sandbox"
// allow-popups -- the ``<base target="_blank">`` link
// rule can open a new tab instead of
// silently dropping the click
// We do NOT grant:
// allow-same-origin / allow-top-navigation -- the iframe
// cannot read parent.document or navigate the host page
// allow-popups-to-escape-sandbox -- popups INHERIT the sandbox
// so an opened tab cannot use ``window.opener.top.location``
// to tabnab the Studio tab. The opened tab loads with an
// opaque origin (some sites will render degraded) which is
// the deliberate trade-off for tabnabbing safety.
sandbox="allow-scripts allow-modals allow-popups"
style={{
width: "100%",
height: iframeHeight,