shadow dom 本身不阻止 document.queryselector 调用,因其查询主文档全局 dom;真正防护需限制执行上下文、约束查询范围至 shadowroot,并配合 closed 模式、composed: false、dompurify 清洗及 iframe 双重隔离。

Shadow DOM 本身不阻止 document.querySelector 在广告脚本中调用——它查的仍是主文档的全局 DOM,这不是 Shadow DOM 的失效,而是你没限制执行上下文。真正要防的是“恶意渗透”,即第三方脚本借查询/操作主站元素破坏布局、窃取数据或触发副作用。关键不在“动态加载”,而在“执行域约束”和“查询范围收束”。
让广告脚本只能查自己内部的 DOM
第三方脚本若写 document.querySelector('header'),它照样能拿到并修改主站 header。这不是 Shadow DOM 没隔离,而是你把脚本放进了全局作用域。正确做法是:所有广告 JS 必须在 Shadow Root 内部创建并执行,且显式绑定作用域:
- 用
const script = document.createElement('script')创建后,append 到 shadowRoot,而非document.head - 脚本内避免直接用
document;改用this.shadowRoot || shadowRoot作为查询起点,例如:shadowRoot.querySelector('.ad-banner') - 若依赖 jQuery 等库,初始化时必须传入 scope:
$(shadowRoot).find('.close-btn').on('click', ...)
切断 document 全局引用的隐式路径
即使脚本在 shadow 内执行,document 对象仍可访问。为防误用或恶意调用,需主动“遮蔽”或“重定向”:
- 在 shadowRoot 中注入一个轻量 wrapper 脚本,临时覆盖
document.querySelector行为(仅对当前 shadow 生效):const orig = document.querySelector;<br>document.querySelector = function(selector) {<br> return shadowRoot.querySelector(selector) || orig.call(document, selector);<br>}; - 更稳妥的做法是:禁止内联脚本直接访问
document,改用type="module"+ CSP nonce,配合import封装查询逻辑,从源头杜绝裸调用
用 closed 模式 + composed: false 阻断穿透意图
开放模式下,外部可通过 el.shadowRoot 反向操作内部节点;而事件若未设 composed: false,点击按钮仍会冒泡到主文档,被监听器捕获并误判为用户行为:
- 创建 shadow root 时强制使用
{ mode: 'closed' },使el.shadowRoot === null,阻断常规 DOM 访问链 - 所有自定义事件 dispatch 前加
{ composed: false },例如:button.dispatchEvent(new Event('ad-close', { bubbles: true, composed: false })) - 宿主元素上禁用 pointer-events 外溢:
:host { pointer-events: auto; } :host * { pointer-events: auto; },防止点击穿透引发意外交互
比隔离更关键:别让它有机会执行危险查询
Shadow DOM 不是沙箱,它不拦截 JS 执行。真正防渗透,靠的是前置约束:
- 广告 HTML 必须经 DOMPurify 清洗后再注入
shadowRoot.innerHTML,过滤掉含javascript:、onerror、eval等风险属性 - 禁用
document.write、document.writeln——它们会直接污染主文档,应在 shadow 内重写为shadowRoot.appendChild()替代 - 优先用 iframe 嵌套广告(
<iframe sandbox="allow-scripts" srcdoc="..."></iframe>),Shadow DOM + iframe 是目前最可靠的双重隔离组合











