不能用自定义元素实现动态脚本注入,因其本身无执行权,但在connectedcallback等生命周期中使用innerhtml、eval等操作会引发xss。安全做法是仅用textcontent、createelement构建结构,富文本须经dompurify净化。

不能也不应该用 HTML 自定义元素(Custom Elements)实现“动态脚本注入逻辑”。这是个危险误区——自定义元素本身不提供执行权,但若开发者在 connectedCallback 或属性变更中主动 eval、innerHTML、new Function 或拼接 script 标签,就等于亲手打开 XSS 大门。
为什么 customElements.define + innerHTML = 高危组合
自定义元素常被误当作“安全封装”,实际它只是 DOM 构建语法糖。一旦你在 connectedCallback 里做这类操作:
class UnsafeWidget extends HTMLElement {
connectedCallback() {
// ❌ 危险:用户可控内容直接进 innerHTML
this.innerHTML = `<div>${this.getAttribute('content')}</div>`;
}
}
攻击者只需传入 <script>fetch('/steal', {method:'POST',body:document.cookie})</script>,就能触发执行。
-
innerHTML不区分来源,只认字符串是否含标签 - 自定义元素生命周期方法(如
attributeChangedCallback)同样可被用户属性触发 - 即使用了
Shadow DOM,若内部仍用innerHTML,防护形同虚设
必须避免的“动态脚本”写法
以下模式在自定义元素中绝对禁止:
- 用
innerHTML渲染用户输入的任意字符串(包括 API 返回值、URL 参数、表单字段) - 调用
eval()、setTimeout(string, ...)、new Function(...)处理用户数据 - 拼接
<script src="..."></script>或<script>...</script>并插入 DOM - 把用户输入直接塞进
onclick、onload等事件属性
哪怕加了 try/catch 或正则过滤,都不可靠——攻击向量太多(如 <img src="x" onerror="...">、javascript:alert(1))。
安全替代方案:只允许结构化数据驱动渲染
真正可控的“动态逻辑”应基于明确 schema 的数据,而非原始 HTML 字符串:
- 用
textContent插入纯文本:this.textContent = this.getAttribute('label') || ''; - 用
createElement+appendChild构建固定结构:const span = document.createElement('span'); span.textContent = userInput; this.appendChild(span); - 若需富文本,走白名单净化流程:先用
DOMPurify.sanitize(htmlString),再赋给innerHTML(且仅限受信上下文) - 属性变更只响应预设键名,如
static get observedAttributes() { return ['size', 'theme']; },拒绝未声明属性
注意:customElements.define 本身不执行脚本;风险永远来自你写的回调函数里怎么处理数据。
CSP 对自定义元素不起兜底作用
即使配置了 Content-Security-Policy: script-src 'self',也拦不住以下情况:
- 用户输入含
<img src="x" onerror="fetch('/leak', {body:localStorage.token})">—— 这是 HTML 属性事件,不走script-src - 通过
location.href或iframe srcdoc注入,绕过 CSP 资源限制 - 利用已加载的合法脚本(如 jQuery)进行原型链污染后执行 payload
自定义元素的安全边界完全由你写的 JS 逻辑决定,不是浏览器自动赋予的。最易被忽略的点是:你以为在封装组件,其实只是把 innerHTML 换了个写法藏进类里。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











