
eslint-plugin-unicorn 之 no-unsafe-dom-html从源码与测试看如何拦截 XSS 高危 DOM HTML API【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicornno-unsafe-dom-html是 eslint-plugin-unicorn 中专门用于拦截 XSS 注入入口的规则它禁止代码使用innerHTML、insertAdjacentHTML、document.write等会解析或插入 HTML 的 DOM API。本文将以 规则文档 为骨架结合 规则源码、单元测试 与 快照测试 逐层拆解它的检测边界、实现原理与迁移方案读完即可在自己的 ESLint 配置中精准启用并落地安全改造。规则定位把 XSS 注入点挡在代码评审之前DOM 中凡是会“把字符串当作 HTML 解析”的 API几乎都是经典的 XSS 注入点sink。no-unsafe-dom-html的作用就是静态地扫描出这些高危调用并给出Do not use unsafe DOM HTML APIs.的报错强制开发者改用更安全的等价 API需要替换元素子节点的 HTML时优先使用Element#setHTML()值应该被当作纯文本处理时优先使用.textContent或.insertAdjacentText()。在 readme.md 的规则总表中该规则的状态列为空它默认未开启也不提供自动修复或建议修复。文档头部还明确标注了它同时被recommended与unopinionated两套预设配置排除在外原因在于 HTML 字符串在部分代码库中是合法的业务输入规则无法在不误伤的前提下默认放行因此需要开发者按项目实际情况手动启用。完整检测范围哪些写法会被拦截文档给出了最核心的违规/安全对照示例下面按类别完整展开所有 ❌ 写法均会被规则命中// ❌ 危险把字符串作为 HTML 解析、插入或替换 element.innerHTML html; element.outerHTML html; iframe.srcdoc html; element.insertAdjacentHTML(beforeend, html); element.setHTMLUnsafe(html); Document.parseHTMLUnsafe(html); range.createContextualFragment(html); iframe.setAttribute(srcdoc, html); document.write(html); document.writeln(html); // ✅ 安全按纯文本处理或使用经过净化语义的解析 API element.setHTML(html); Document.parseHTML(html); element.textContent text; element.insertAdjacentText(beforeend, text);注意Document.parseHTML(html)与element.setHTML(html)是允许的它们虽然也解析 HTML但语义上要求配合清理机制sanitizer属于文档推荐的安全迁移目标而parseHTMLUnsafe/setHTMLUnsafe明确不做清理因此被禁止。源码剖析三类检测机制与判定算法规则实现位于 rules/no-unsafe-dom-html.js整体由三块检测逻辑组成分别对应不同的危险面。1. 危险赋值属性innerHTML/outerHTML/srcdoc源码第 9-13 行用一个Set定义了三个“危险赋值属性”const htmlAssignmentProperties new Set([ innerHTML, outerHTML, srcdoc, ]);规则在MemberExpression上挂接监听源码 rules/no-unsafe-dom-html.js#L126-L139先取出成员表达式的静态属性名再调用isHtmlSetter(node)判断该成员是否处于“写入位置”。只有同时满足“属性名匹配”且“处于写入上下文”才会报错因此读取操作不受影响element.innerHTML; // ✅ 读取不报错 const html element.outerHTML; // ✅ 读取不报错 delete element.innerHTML; // ✅ 不是写入不报错 element.innerHTML; // ✅ 更新表达式UpdateExpression不在检测范围内不报错而以下各种写入形态都会被拦截对应 test/no-unsafe-dom-html.js 中的 invalid 用例element.innerHTML html; // 普通赋值 element.innerHTML html; // 复合赋值 element.outerHTML || html; // 逻辑赋值 element[innerHTML] html; // 计算成员字符串字面量 element[innerHTML] html; // 模板字面量 for (element.innerHTML in object) {} // for-in 左值 for (element.innerHTML of list) {} // for-of 左值 ({html: element.innerHTML} source); // 解构赋值 [element.outerHTML] source; // 数组解构 [...element.innerHTML] source; // rest 元素 ({...element.innerHTML} source); // 对象 rest这正是isHtmlSetter源码 rules/no-unsafe-dom-html.js#L84-L116逐一识别的六种写入位置AssignmentExpression、AssignmentPattern、ForInStatement/ForOfStatement左值、ArrayPattern元素、RestElement参数、以及作为ObjectPattern属性值的情形。2. 危险方法调用insertAdjacentHTML/setHTMLUnsafe/createContextualFragment第二组Set源码 rules/no-unsafe-dom-html.js#L15-L18覆盖三个危险方法const htmlMethods new Set([ createContextualFragment, insertAdjacentHTML, setHTMLUnsafe, ]);规则在CallExpression上监听源码 rules/no-unsafe-dom-html.js#L141-L154对调用方成员表达式做静态属性名匹配。同样地方法名的写法可以变形但都会被识别element.insertAdjacentHTML(beforeend, html); // ❌ element?.insertAdjacentHTML(beforeend, html); // ❌ 可选链 elementinsertAdjacentHTML; // ❌ 计算成员 elementinsertAdjacentHTML; // ❌ 模板字面量 range.createContextualFragment(html); // ❌ range?.createContextualFragment(html); // ❌3. 全局 API 追踪document.write/document.writeln/Document.parseHTMLUnsafe这三类全局 API 无法用“成员表达式 静态属性名”的方式一概处理因为document、Document是全局对象。规则借助了项目自研的 GlobalReferenceTracker基于eslint-community/eslint-utils的ReferenceTracker在源码 rules/no-unsafe-dom-html.js#L69-L82 中注册了三个追踪器const globalHtmlMethodTrackers [ createGlobalHtmlMethodTracker(Document.parseHTMLUnsafe), createGlobalHtmlMethodTracker(document.write), createGlobalHtmlMethodTracker(document.writeln), ];GlobalReferenceTracker会在Program退出时遍历全局引用listen方法其核心价值是顺着变量引用链追踪别名调用——这一点在快照测试中有非常直观的体现const parseHTMLUnsafe Document.parseHTMLUnsafe; parseHTMLUnsafe(html); // ❌ 被命中报错落在别名调用上 const write document.write; write(html); // ❌ 被命中 const doc document; doc.write(html); // ❌ 被命中对象别名同时全局追踪意味着window.document.write(html)、globalThis.document.writeln(html)、window.Document.parseHTMLUnsafe(html)、globalThis.Document.parseHTMLUnsafe(html)这些经由全局对象绕行的写法同样会被拦截见快照 invalid 20/21/36/37。4.iframe.setAttribute(srcdoc, …)特判srcdoc属性还有一条隐蔽的注入路径——通过setAttribute写入。源码 rules/no-unsafe-dom-html.js#L60-L67 的isSrcdocSetAttributeCall专门处理该场景要求调用方是setAttribute成员调用、参数数量 ≥ 2并且第一个参数解析为字符串字面量后小写化等于srcdoc因此srcDoc这种大小写变体也会被拦截而data-srcdoc、srcdoc变量等不会iframe.setAttribute(srcdoc, html); // ❌ iframe.setAttribute(srcDoc, html); // ❌ 大小写变体 iframe.setAttribute(srcdoc, html); // ❌ 模板字面量 iframe.setAttribute(data-srcdoc, html); // ✅ 属性名不匹配 iframe.setAttribute(srcdoc, html); // ✅ 非静态参数无法判定 iframe.setAttribute(srcdoc); // ✅ 参数不足不是写入 setAttribute(srcdoc, html); // ✅ 非成员调用静态属性名解析与可选链解包规则对“属性/方法名”的识别依赖getStaticPropertyName源码 rules/no-unsafe-dom-html.js#L21-L32非计算成员element.innerHTML直接取Identifier的name计算成员element[innerHTML]、element[\innerHTML]调用getStaticStringValue 解析字符串字面量/无插值模板字面量的静态值。因此element[innerHTML] html变量名与elementmethod动态方法名这类无法静态求值的写法不会被误报——这既是能力边界也避免了把运行时动态属性访问误判为危险调用。可选链场景由unwrapChainExpression源码 rules/no-unsafe-dom-html.js#L34-L37处理element?.insertAdjacentHTML(...)、range?.createContextualFragment(...)在 AST 中是ChainExpression包裹的成员调用规则会先解包再匹配方法名所以可选链写法同样会被命中对应快照 invalid 13/26/29/34/35。规则有意不去覆盖的边界文档明确交代了三类刻意不做的检测这决定了规则的误报率下限Trusted Types 兼容性规则不试图识别值是否为TrustedHTML对象。Trusted Types 的安全性依赖全项目 CSP内容安全策略的强制程度与策略本身的质量这是本地 ESLint 规则无法可靠证明的。若项目确实全面采用 Trusted Types应在具体 sink 处用简短注释关闭该规则而不是全局关闭。间接调用通过.call()、.apply()、.bind()发起的调用不在检测范围。快照测试中document.write.call(document, html)、element.insertAdjacentHTML.apply(element, [beforeend, html])、Document.parseHTMLUnsafe.bind(Document)(html)等全部列为 valid 用例。动态属性名elementmethod、rangemethod、Documentmethod等非静态属性访问无法判定均不报错。此外全局追踪器对变量遮蔽是敏感的如果document、Document被函数参数或局部变量遮蔽规则不会误报例如快照中的function render(document) { document.write(html); }与const document {write() {}}; document.write(html);都是合法用例——因为它们引用的是用户自定义对象而非全局document。规则元数据与启用方式从 rules/no-unsafe-dom-html.js#L160-L174 可以看到规则元数据type: problem报告的是真实的潜在安全问题而非风格问题schema: []无任何可配置选项规则开箱即用、行为固定languages: [js/js]仅作用于 JavaScript/JSX 文件不参与 CSS 等非 JS 语言处理不提供fixable标记也不带suggestions因此只能报错、不能自动改写。由于规则在recommended与unopinionated预设中均为禁用状态需要手动启用。例如在 flat config 中import unicorn from eslint-plugin-unicorn; export default [ { plugins: {unicorn}, rules: { unicorn/no-unsafe-dom-html: error, }, }, ];若某个 sink 是业务上故意为之如渲染受信任的富文本按文档建议用短注释局部豁免// unicorn:disable no-unsafe-dom-html —— 该 HTML 来自受信任的 CMS 富文本字段 element.innerHTML trustedRichText;测试验证48 个非法用例的快照证据规则的判定逻辑被 test/no-unsafe-dom-html.js 以 49 个 valid 用例与 48 个 invalid 用例完整覆盖全部报告细节沉淀在 快照文件 中。几个值得留意的快照细节报错位置精确落在属性/方法名上如element.innerHTML html高亮innerHTML便于快速定位换行写法不影响检测element\n\t.innerHTML html;依然命中invalid 48多行调用同样命中element.insertAdjacentHTML(\n\tbeforeend,\n\thtml,\n);报错锚定在方法名上invalid 49别名调用会把报错指向别名调用处本身如write(html)高亮write提示明确。与相邻规则的联动innerHTML相关的话题在插件中并不是孤立的文档与源码中可观察到两类联动prefer-dom-node-replace-children.md 明确提到非空的.innerHTML赋值由prefer-dom-node-html-methods与no-unsafe-dom-html协同处理——前者推动用replaceChildren(...)等 DOM 方法替换后者负责拦截真正危险的 HTML 注入场景规则文件在 rules/index.js 中导出注册与插件其余 300 条规则共用同一套元数据与测试基建。实践中推荐组合对以文本/节点操作为主的代码库启用prefer-dom-node-html-methods做正向引导再对确实存在的 HTML 字符串写入启用no-unsafe-dom-html做负向拦截配合prefer-dom-node-text-content、prefer-dom-node-append等规则即可把前端 DOM 写入面整体收敛到安全 API 集合内。小结何时启用如何迁移no-unsafe-dom-html适合所有涉及浏览器 DOM 写入的前端项目尤其是在渲染用户输入、富文本、模板拼接 HTML场景较多的代码库中价值最大。它通过“危险赋值属性 危险方法 全局 API 别名追踪 srcdoc特判”四层机制把已知的 HTML 注入 sink 静态暴露出来同时通过静态属性名解析、isHtmlSetter写入位置判定和有意保留的边界Trusted Types、间接调用、动态属性名、变量遮蔽把误报控制在可接受范围。迁移时只需把innerHTML/outerHTML/srcdoc赋值换成setHTML、把insertAdjacentHTML换成insertAdjacentText、把document.write系列替换为显式的节点或文本插入即可规则本身无需任何配置即可作为防线常驻。【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考