diff --git a/demo-site/src/components/ShadowDomEmbed.tsx b/demo-site/src/components/ShadowDomEmbed.tsx index e004850..1bb9fef 100644 --- a/demo-site/src/components/ShadowDomEmbed.tsx +++ b/demo-site/src/components/ShadowDomEmbed.tsx @@ -20,9 +20,34 @@ import { INJECTIONS } from "../data/injection-fixtures"; // second card exercises the Tier 3 adopted-stylesheet path: that // class only matches via the EasyList generic CSS sheet, which now // adopts into open shadow roots so the hide applies here too. +// +// A second host below mounts a custom element with a CLOSED shadow root +// to exercise `closed-shadow-root-annotate`. By spec the rules cannot +// see inside; the heuristic only confirms it's there. + +const CLOSED_TAG = "abs-closed-widget"; + +if (typeof customElements !== "undefined" && !customElements.get(CLOSED_TAG)) { + customElements.define( + CLOSED_TAG, + class extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: "closed" }); + const inner = document.createElement("div"); + inner.style.cssText = + "padding:6px 10px;border:1px solid #7c3aed;background:#f5f3ff;color:#4c1d95;font:13px system-ui;border-radius:4px;"; + inner.textContent = + "Closed-shadow widget — contents are invisible to ABS by spec."; + shadow.append(inner); + } + }, + ); +} export default function ShadowDomEmbed() { const hostRef = useRef(null); + const closedHostRef = useRef(null); useEffect(() => { const host = hostRef.current; @@ -75,6 +100,18 @@ export default function ShadowDomEmbed() { }; }, []); + useEffect(() => { + const host = closedHostRef.current; + if (!host || host.querySelector(CLOSED_TAG)) { + return; + } + const widget = document.createElement(CLOSED_TAG); + host.append(widget); + return () => { + widget.remove(); + }; + }, []); + return (
+

+ The widget below mounts inside a closed shadow root. ABS cannot + reach inside by spec — every rule passes its contents through untouched. + With closed-shadow-root-annotate enabled, a + screen-reader-only landmark is prepended to the document noting that the + page is using closed shadow roots and content here is invisible to the + extension. +

+
); } diff --git a/docs/src/content/docs/rules.md b/docs/src/content/docs/rules.md index 77e6e3b..773affe 100644 --- a/docs/src/content/docs/rules.md +++ b/docs/src/content/docs/rules.md @@ -40,7 +40,9 @@ Any content a page renders inside a closed shadow root — whether ads, chat widgets, hidden text, or prompt-injection payloads — is invisible to every rule and will be passed through to the agent untouched. Closed shadow roots are uncommon outside browser UA shadows and a handful of hardened embeds, but they -are a known gap. +are a known gap. The optional +[Flag Closed Shadow Roots](#flag-closed-shadow-roots-experimental) rule can +heuristically warn the agent at read-time when this gap is in use. ## Indirect prompt injection @@ -426,6 +428,34 @@ indirect-prompt-injection delivery surface. Search-engine cloaking has a long lineage [[19]](#ref-wu-cloaking); the same primitive aimed at LLM crawlers is the new threat. +#### Flag Closed Shadow Roots (Experimental) + +- **ID:** `closed-shadow-root-annotate` +- **Default:** off +- **Top frame only** + +Heuristically detect pages that render content inside closed shadow roots and +prepend a screen-reader-only landmark noting that the extension cannot see +inside those shadow trees. Complements the open-shadow-root coverage described +in [Coverage scope](#coverage-scope) above by giving the agent a positive signal +at read-time that a known blind spot is in use on this page. + +Detection is necessarily heuristic: by spec, an element with `mode: "closed"` is +indistinguishable from an element with no shadow root at all from outside +JavaScript. The rule looks for the structural shape strongly correlated with +"closed shadow host": an upgraded custom element (hyphenated tag name, defined +in `customElements`) with no light-DOM children, no `host.shadowRoot`, and a +non-zero rendered box. Built-in elements with UA shadow roots (``, +`
`, `