前端 PDF 生成的安全问题常被忽略,开发者的注意力都在功能与性能上,但恰恰是把 HTML 变成 PDF 的过程,隐藏着多个安全攻击面。dompdf.js 的 addPage 接收 HTML 字符串或 DOM 元素,这意味着任何被拼进模板的用户输入,都会经过解析、渲染并最终进入 PDF——如果输入包含恶意脚本或链接,可能引发 XSS、钓鱼内容注入、外部资源窃取等风险。好消息是这些风险都有成熟的对策:用 DOMPurify 清洗输入、用 CSP 约束页面能力、校验所有外部 URL、避免把敏感数据写进可被截获的地方、用 PDF 加密保护文件内容。本文按攻击面逐一展开:先讲清楚前端 PDF 生成的安全模型,再深入 XSS 的三种典型场景与 DOMPurify 的正确用法,然后讨论 CSP 与外部资源策略、敏感数据的保护、PDF 加密与权限控制,最后给出一份可直接执行的安全清单。安全不是发布前的补丁,而是从模板设计就遵守的约束,读完本文你就能把 PDF 生成功能做成经得起审计的模块。
dompdf.js 在浏览器中运行,输入是 HTML,输出是 PDF 文件,这个模型下的安全边界与常规页面渲染不同:攻击者控制的字符串如果进入模板,会被当作结构解析,脚本、链接、图片引用都可能被执行或嵌入。与后端 PDF 生成相比,前端方案少了服务端这道过滤关卡,输入清洗的责任完全落在前端代码上,任何拼接模板的地方都是潜在注入点。
明确威胁模型是安全工作的起点:你的 PDF 可能包含用户提交的姓名、地址、评论等自由文本,也可能包含订单号、金额等业务数据,还可能引用用户提供的图片 URL。不同数据的风险等级不同——自由文本需要清洗,业务数据需要防篡改,外部 URL 需要校验来源。把输入按来源和可信度分级,防护措施才能有的放矢,而不是一刀切地禁止所有功能。
另一个容易忽视的边界是输出侧:PDF 文件本身会离开浏览器,被下载、转发、存储,一旦生成,你就失去了对内容的控制。这意味着敏感数据要么根本不进 PDF,要么在生成时就用加密与权限控制保护起来。设计阶段就要问:这份 PDF 会包含什么敏感信息?谁有权查看?泄露的后果是什么?答案决定安全投入的力度。
第一种是 HTML 注入:用户输入被直接拼进模板字符串,比如 name 字段包含 script 标签或带 onerror 属性的 img 标签,解析时恶意代码进入页面上下文。虽然 PDF 渲染环境对脚本执行有限制,但预览环节、模板复用场景下脚本可能实际运行,且注入内容本身就会污染 PDF 的观感与可信度,绝不能放任。
第二种是 URL 注入:用户提供的链接或图片地址被写进 href、src 属性,恶意值可以是 javascript: 伪协议、data: 大体积载荷,或指向内网地址的探测请求。浏览器在解析与加载时可能触发这些行为,轻则功能异常,重则成为钓鱼与信息探测的载体,所有外部 URL 在进模板前必须经过协议白名单校验。
第三种是属性逃逸:用户输入被放进 HTML 属性时,如果包含未转义的引号或特殊字符,可以闭合属性并注入新标签,比如 style 属性里的表达式、class 属性里的意外结构。这类注入比直接拼标签更隐蔽,清洗时容易漏网。正确做法是统一走 DOMPurify 这类成熟清洗器,而不是手写正则——手写过滤永远追不上攻击者的想象力。
DOMPurify 是前端 HTML 清洗的事实标准,它把输入解析成 DOM,再按白名单重建,任何不在名单里的标签、属性和协议都会被剥离,输出是干净的 HTML 字符串。与正则过滤不同,DOMPurify 基于真正的 DOM 解析,能够正确处理嵌套、实体与各种编码变体,攻击者很难找到绕过路径,这是它被广泛信任的原因。
白名单按需收紧:PDF 模板真正需要的标签与属性才放行,img 的 src 要限制为 http/https 协议(DOMPurify 默认已做),style 属性如果业务不需要可以直接禁掉。示例中 ALLOW_DATA_ATTR 设为 false,避免 data-* 属性被当作传参通道。白名单越窄,攻击面越小,这是清洗配置的第一原则。
注意清洗发生在拼接之前:先清洗用户输入,再拼进模板,顺序不能颠倒。模板自身的静态结构可以信任,但任何动态数据——订单号、用户名、备注——都必须经过 escapeHtml 或 DOMPurify,二选一,不能漏。把这条规则写进代码评审清单,让每个新增的模板拼接点都经过检查,XSS 的入口才会真正堵死。
import DOMPurify from 'dompurify';
import { DomPDF } from 'dompdf.js';
const POLICY = {
ALLOWED_TAGS: [
'p', 'h1', 'h2', 'h3', 'strong', 'em', 'ul', 'ol', 'li',
'table', 'thead', 'tbody', 'tr', 'td', 'th', 'br',
'span', 'div', 'img', 'a', 'blockquote', 'code', 'pre',
],
ALLOWED_ATTR: ['src', 'alt', 'href', 'width', 'height', 'style', 'class'],
ALLOW_DATA_ATTR: false,
};
function buildPdfFromUserInput(userHtml, orderId) {
const safeHtml = DOMPurify.sanitize(userHtml, POLICY);
const template = `<h1>订单 ${escapeHtml(orderId)}</h1>${safeHtml}`;
const pdf = new DomPDF();
pdf.addPage(template, { format: 'A4' });
pdf.save('order.pdf');
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
}[c]));
}
Content-Security-Policy 是纵深防御的第二层:即使输入清洗出现漏洞,CSP 也能限制恶意代码的能力。建议为包含 PDF 生成功能的页面配置严格的默认策略——script-src 只允许同源与必要的 CDN、img-src 限制图片来源、connect-src 限制请求目标。CSP 不能替代清洗,但能让单点失误不演变成安全事件,两者叠加才构成完整防线。
外部资源是另一个重点:模板里引用的字体、图片、CSS 都来自网络,攻击者可以构造指向内网 IP 或本地服务的 URL,借浏览器发起探测请求,这就是常说的 SSRF 变体。对策是校验所有外部 URL 的协议必须是 http/https、域名进入白名单或至少经过格式校验,图片与字体资源优先自托管,把不可控的外部依赖降到最低。
资源加载还要考虑完整性:为关键静态资源启用 Subresource Integrity(SRI),浏览器会校验资源哈希,CDN 被篡改时直接拒绝加载,避免供应链攻击。wasm 文件、核心 JS、模板依赖的字体都可以加 integrity 属性。这些策略配置一次、长期生效,是安全投入产出比最高的环节之一,值得在部署阶段就落实。
PDF 里的敏感数据有三个泄露途径:下载后被随意转发、存储在不可信的位置、被日志或监控系统截获。生成侧能做的是最小化暴露——模板只包含业务必需的数据,身份证号、手机号等敏感字段按需打码,避免完整信息进 PDF;日志侧避免把模板内容或用户数据打进日志,错误上报只带错误码与摘要。
dompdf.js 支持 PDF 加密与权限控制,可以为生成的文档设置用户密码与所有者密码,并限制打印、复制等操作。对包含机密信息的文档,生成时开启加密是对内容的基本尊重;对需要长期存档的文档,加密还能防止内容被轻易提取。加密密码的管理要纳入密钥体系,避免硬编码在代码里。
权限控制要结合业务场景设计:合同类文档通常允许查看与打印但限制复制,内部报表可能限制打印,对外交付的文件可以只读。把加密与权限参数化到导出服务层的配置里,不同文档类型走不同策略,既不增加业务代码复杂度,又能保证安全要求一致的执行,审计时也一目了然。
可直接执行的安全清单:所有用户输入经 DOMPurify 清洗后再拼模板;动态数据一律 escapeHtml;img、a 的 URL 只允许 http/https 且经过校验;配置 CSP 并覆盖 script、img、connect 三类指令;关键资源启用 SRI;wasm 与字体自托管或同源部署;敏感字段打码后再入 PDF;机密文档启用加密与权限控制;日志不含模板内容与用户数据;安全配置纳入代码评审与发布检查。
常见误区一:认为前端生成没有服务端,就不存在注入风险。实际恰恰相反,清洗责任全部落到前端,漏一处就是一处漏洞。误区二:用正则过滤代替成熟清洗器。正则无法覆盖编码变体与嵌套结构,DOMPurify 是经过实战检验的方案,没有理由不用。误区三:只在发布前做一次安全检查。模板与功能持续演进,安全清单要跟着迭代,每次新增拼接点都重新过一遍。
把安全实践固化到流程里:模板拼接点必须经过评审、依赖版本升级时跑一遍安全回归、DOMPurify 与 dompdf.js 保持更新以获取安全修复。安全不是一次性的补丁工程,而是持续的过程约束。当每个开发者都默认输入先清洗、URL 先校验、敏感数据最小化,PDF 生成功能就能长期保持安全,经得起审计与攻击的考验。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。