cure53/DOMFortify

GitHub: cure53/DOMFortify

DOMFortify 通过接管浏览器 Trusted Types default 策略,在不修改代码的情况下自动净化遗留 HTML sink,为存在 XSS 风险的 Web 应用提供零改动的安全加固。

Stars: 17 | Forks: 1

# DOMFortify [![License: MPL-2.0 OR Apache-2.0](https://img.shields.io/badge/license-MPL--2.0%20OR%20Apache--2.0-blue.svg)](LICENSE) [![Build & Test](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/21f47c46ff021036.svg)](https://github.com/cure53/DOMFortify/actions/workflows/build-and-test.yml) [![CodeQL](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/b1ceb71c1d021041.svg)](https://github.com/cure53/DOMFortify/actions/workflows/codeql-analysis.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cure53/DOMFortify/badge)](https://scorecard.dev/viewer/?uri=github.com/cure53/DOMFortify) DOMFortify 会为页面开启 Trusted Types,并悄无声息地接管浏览器的 `default` 策略, 这样一来,像 `el.innerHTML = location.hash` 这样存在漏洞的旧代码在触及 DOM 之前就会先被净化。你不需要改动代码,甚至都不需要知道 bug 在哪里。 它是为那些你难以轻松修复的站点而生的:没人愿意碰的复杂应用或遗留应用、你无法打补丁的第三方 widget, 以及在人们听说过 XSS 之前就写好的 2000 多个 `innerHTML` sink。 **只需发布该策略,浏览器就会自动使用 DOMPurify 或其他 sanitizer 保护每一个 HTML sink。** ## 有 Demo 吗? 当然有。[体验一下 DOMFortify](https://cure53.de/fortify) - 向一个故意留有漏洞的页面发送 payload, 看着浏览器在它们到达 DOM 之前将其化解。 ## 工作原理 Trusted Types 允许页面注册一个 `default` 策略,浏览器会在遇到每个危险的 sink 时调用它。DOMFortify 就是这个策略。 HTML 会通过 [DOMPurify](https://github.com/cure53/DOMPurify) (或你提供给它的任何 sanitizer)进行处理;像 `eval` 和 `script.src` 这样的脚本 sink 会被直接拒绝, 因为没有任何安全的方法可以净化可执行代码。 ## 用法 分为两个部分。首先,通过 CSP 开启强制执行 —— 如果可以设置响应头,请使用响应头: ``` Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default dompurify; ``` ...或者如果你无法设置任何响应头,可以通过 `` 标签: ``` ``` 其次,加载 sanitizer,然后加载 DOMFortify,**放在 `` 中的最前面**,要排在攻击者可能触及的任何内容之前。 请使用 SRI 固定这两者的完整性,这样即使 CDN 出现问题,也会以防御失效(关闭)而非暴露漏洞(开启)的方式应对: ``` ``` 就是这样。该脚本会在加载时自行安装。想检查它是否真的起作用了? ``` DOMFortify.status().protected; // true when enforced, owning the policy, and sanitizer ready ``` 如果你必须通过 bundler 进行构建,请导入模块构建版本并尽早调用 `init()` - 但要明白,bundler 不会把你的代码放在最前面,而这正是它所必需的: ``` import { init } from 'domfortify'; init(); ``` ## 配置 在 script 标签之前设置 `window.DOMFortifyConfig`,或者将相同的对象传递给 `DOMFortify.init()`。 每个选项都是可选的;默认配置会对每个 HTML sink 使用 DOMPurify,并对每个脚本 sink 采取硬性拒绝。 配置仅读取一次,且只读取自身属性(own-properties),因此被污染的原型(prototype)无法偷偷注入 值或放宽拒绝策略。 下面的每个主题都在 [`/demos`](demos/) 中提供了一个可运行的页面 - 这些链接会直接指向它们。 ### 选择 sanitizer ``` // Default: window.DOMPurify sanitizes every HTML sink. window.DOMFortifyConfig = { SANITIZER: window.DOMPurify }; // Any object with .sanitize(input, config) works... window.DOMFortifyConfig = { SANITIZER: myCustomSanitizer }; // ...as does a bare (string) => string function, e.g. the native Sanitizer API. window.DOMFortifyConfig = { SANITIZER: (s) => { const d = document.createElement('div'); d.setHTML(s); return d.innerHTML; }, }; // SANITIZER_CONFIG is forwarded verbatim as the sanitizer's second argument (a DOMPurify config here). window.DOMFortifyConfig = { SANITIZER_CONFIG: { ALLOWED_TAGS: ['b', 'i', 'a'], ALLOWED_ATTR: ['href'] }, }; ``` Demo:[sanitizer 配置](demos/config-demo.html),[原生 Sanitizer API](demos/native-sanitizer-demo.html)。 ### 允许脚本 sink ``` // Script sinks (eval, javascript: URLs, script.src, Worker URLs) are REFUSED by default. // A hook may allow specific, vetted values: return a string to mint it, anything else to refuse. window.DOMFortifyConfig = { ALLOW_SCRIPT: (code) => null, // default: refuse every eval-like sink ALLOW_SCRIPT_URL: (url) => (url.startsWith('https://cdn.example/') ? url : null), // allow one origin }; ``` Demo:[允许一个脚本 URL](demos/allow-script-url-demo.html)。 ### 通过 URL 进行作用域限制 ``` // EXCLUDE: URL pattern(s) where DOMFortify stays completely inactive - no policy, no meta injection. // It does NOT install a passthrough (that would be a silent XSS hole). Matched against location.href: // a string is a substring match, a RegExp is test()ed, and either may be given as an array. window.DOMFortifyConfig = { EXCLUDE: ['/admin/', /\/internal\b/] }; // URL_CONFIG: per-URL overrides; the FIRST matching rule's own keys override the base config. Handy // for a stricter (or looser) sanitizer config, sanitizer, or script hook on specific routes. window.DOMFortifyConfig = { SANITIZER_CONFIG: { ALLOWED_TAGS: ['b', 'i'] }, // the baseline URL_CONFIG: [{ match: '/comments/', SANITIZER_CONFIG: { ALLOWED_TAGS: ['b'] } }], }; ``` Demo:[通过 URL 进行作用域限制](demos/url-config-demo.html)。 ### 开启强制执行(高级) DOMFortify 本身不启用 Trusted Types - 这是由 CSP 完成的,而响应头是唯一完全可靠的途径。 对于既不能设置响应头也不能手动放置 `` 的页面,`INJECT_META` 是一个可选的、尽力而为的 后备方案(注意事项请参见 [它不会做什么](#what-it-wont-do))。 ``` // Opt-in, default false. Best-effort: a CSP is honored only when the parser inserts it, so // this can work only when DOMFortify runs during the initial parse and only for content parsed // afterwards. Otherwise it appends a non-enforcing node and reports status().metaInjected === false. window.DOMFortifyConfig = { INJECT_META: true }; // META_DIRECTIVE overrides the whole trusted-types directive, e.g. if your policy names differ. window.DOMFortifyConfig = { INJECT_META: true, META_DIRECTIVE: "require-trusted-types-for 'script'; trusted-types default dompurify my-policy;", }; ``` Demo:[meta 注入](demos/meta-inject-demo.html)。 ### 监控(仅报告模式) ``` // ON_VIOLATION fires on every notable decision: refusals, allowed sinks, a missing sanitizer, an // excluded URL, and so on. Wire it to your telemetry as a safe on-ramp before you rely on enforcement. window.DOMFortifyConfig = { ON_VIOLATION: (code, detail) => console.warn('[DOMFortify]', code, detail), }; ``` Demo:[仅报告模式监控](demos/report-only-demo.html)。 ## 它不会做什么 这是一种补救措施,而不是魔法。请了解其局限性: - **它需要 CSP。**没有强制执行就没有保护 - 并且它会通过 `status()` 告知你这一点。 - **`INJECT_META` 是尽力而为的。**除非解析器在初始解析期间插入,否则通过脚本插入的 `` CSP 会被忽略。 在可以选择使用响应头或手动放置 `` 的情况下,不要依赖它;请通过检查 `status()` 来查看强制执行是否真正生效。 - **必须最先加载它。**谁先注册了 `default` 策略,谁就占据主导。如果攻击者的代码抢先一步, 你的处境会比以前更糟。不要添加 `'allow-duplicates'`。 - **一次仅限一个 realm。**每个 iframe 都是独立的世界,需要加载各自的 DOMFortify。 - **仅限 Trusted Types sink。**内联事件处理器(`onclick=`)、`style` 以及 `href` URL 并不是 TT 的 sink。请通过真正去除了 `'unsafe-inline'` 的 `script-src` 来关闭这些漏洞。 - **仅限一种 sanitizer。**sanitizer 中的绕过漏洞意味着它所防护的一切都会被绕过。 ## 安全 发现了漏洞?请私下报告 - 参见 [SECURITY.md](SECURITY.md)。请勿公开提交 issue。 建立在 Frederik Braun 的 [Perfect types with setHTML()](https://frederikbraun.de/perfect-types-with-sethtml.html) 和 Jun Kokatsu 的“Perfect Types”的基础之上。作者为 [Cure53](https://cure53.de)。 ## 前人工作 DOMFortify 建立在成熟的浏览器和生态概念之上,而不是声称从零发明了基于 Trusted Types 的 HTML 消毒。其底层强制执行机制是浏览器原生的 [Trusted Types API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API),且通常与 DOMFortify 配合使用的 sanitizer [DOMPurify](https://github.com/cure53/DOMPurify) 早已提供了 Trusted Types 集成。诸如 [`melloware/csp-webpack-plugin`](https://github.com/melloware/csp-webpack-plugin) 及其 Rspack 对应的 [`rspack-contrib/csp-rspack-plugin`](https://github.com/rspack-contrib/csp-rspack-plugin) 等早期工具,也展示过通过安装由 DOMPurify 支持的 `default` Trusted Types 策略来为遗留 `innerHTML` 式 sink 改造防护的理念。 DOMFortify 的不同之处在于其侧重点和打包形式:它是一个独立的 runtime 加固层,而不是 bundler 端的 CSP 助手;并且它强调对类脚本 sink 采用更安全的默认设置、sanitizer 抽象化、路由感知配置、CSP/遥测集成,以及对配置和原型污染等边缘情况的防御性处理。生态系统中的相关工作还包括面向框架或类型系统的方案,例如 [Angular 的 Trusted Types 集成](https://angular.dev/best-practices/security)、Google 的 [`safevalues`](https://github.com/google/safevalues),以及 [W3C Trusted Types 项目](https://github.com/w3c/trusted-types/wiki/Integrations) 收集的早期 Trusted Types 集成方案。
标签:DOMPurify, DOM净化, Trusted Types, Web安全, XSS防御, 后端开发, 数据可视化, 蓝队分析