From b97abca72ad8e0fa6748a6dfd3cca1a2c4b5ae2b Mon Sep 17 00:00:00 2001 From: Ren Koya Date: Mon, 10 Aug 2026 10:08:13 +0900 Subject: [PATCH] fix: strip the opener from every link a Gadget UI opens The Gadget iframe carries allow-popups-to-escape-sandbox, so a link it opens lands in an unsandboxed context; the injected click handler exists to make sure that context has no opener. It only matched `a[href][target]` with `target === "_blank"`, and two shapes slip past: - a link with no target attribute of its own, in a document carrying ``. CSP's base-uri restricts a base element's href, not its target, so Gadget code can set the default target for every link on the page, and `a.target` stays "". - an SVG anchor, whose `.target` is an SVGAnimatedString, so `.toLowerCase()` throws and the handler never reaches the rel write. Match any anchor instead of deciding which ones open a new context. rel=noopener is inert on a same-context navigation, so over-applying it costs nothing, and neither shape can slip through. --- packages/workshop-frontend/src/GadgetUI.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/workshop-frontend/src/GadgetUI.tsx b/packages/workshop-frontend/src/GadgetUI.tsx index 8bc6bdfa..1a790e8d 100644 --- a/packages/workshop-frontend/src/GadgetUI.tsx +++ b/packages/workshop-frontend/src/GadgetUI.tsx @@ -66,13 +66,19 @@ window.addEventListener('keydown', (event) => { } }, true); +// Strip the opener from every link the user clicks. This deliberately does not try to work out +// which links open a new context: a link with no target attribute of its own still opens one when +// the document carries a , which the CSP's base-uri does not restrict, and an +// SVG anchor's .target is an SVGAnimatedString rather than a string. Since the frame's popups +// escape the sandbox, missing one matters; adding rel=noopener to a same-context navigation +// does not, because it is inert there. window.addEventListener('click', (event) => { if (!(event.target instanceof Element)) { return; } - const anchor = event.target.closest('a[href][target]'); - if (!anchor || anchor.target.toLowerCase() !== '_blank') { + const anchor = event.target.closest('a[href]'); + if (!anchor) { return; }