PDF 不只能看:交互、表单与安全
7.1 PDF 中的 JavaScript
7.1.1 是的,PDF 能跑代码
很多人不知道这一点:PDF 从 1.3 版本(1999 年)开始支持嵌入 JavaScript。
这不是"类 JavaScript 的脚本"——它是标准的 ECMAScript,加上 Adobe 定义的一套 PDF 专用 API。你可以在 PDF 中:
- 验证表单输入(比如检查邮箱格式)
- 执行数学计算(自动算总价)
- 动态显示/隐藏表单字段
- 弹出对话框
- 与外部数据库通信(通过 SOAP/HTTP)
- 操控 PDF 的页面和注释
一段 PDF 内嵌 JavaScript 的示例:
// 当"数量"字段改变时,自动计算总价
var quantity = this.getField("quantity").value;
var price = this.getField("unitPrice").value;
var total = quantity * price;
this.getField("totalPrice").value = total.toFixed(2);
7.1.2 JavaScript 在 PDF 中的位置
JavaScript 通过以下方式嵌入 PDF:
% 文档级 JavaScript(打开 PDF 时执行)
<< /Type /Catalog
/Names << /JavaScript << /Names [(init) 10 0 R] >> >>
>>
10 0 obj
<< /S /JavaScript /JS (app.alert\("Hello from PDF!"\)) >>
endobj
触发时机:
- 文档打开时(Document Open Action)
- 页面打开/关闭时
- 表单字段值改变时
- 按钮点击时
- 打印/保存前后
7.1.3 安全隐患
一份看似无害的 PDF 文档可以在你打开它的瞬间执行任意 JavaScript 代码。虽然 PDF 的 JS 引擎有沙箱限制(不能直接读写本地文件系统),但历史上出现过大量沙箱逃逸漏洞:
| 年份 | CVE | 影响 |
|---|---|---|
| 2009 | CVE-2009-0927 | Acrobat JS heap overflow,远程代码执行 |
| 2010 | CVE-2010-0188 | TIFF 解析 + JS 组合攻击 |
| 2013 | CVE-2013-0640 | Acrobat 沙箱逃逸 |
| 2018 | CVE-2018-4990 | Acrobat JS UAF 漏洞 |
| 2023 | CVE-2023-26369 | Acrobat out-of-bounds write |
这就是为什么安全专家建议:不要用 Adobe Acrobat 打开来源不明的 PDF。用浏览器(有额外沙箱层)或 Sumatra(根本不执行 JS)更安全。
7.2 表单系统
7.2.1 AcroForm:PDF 原生表单
PDF 1.2 引入了 AcroForm——一套内置的交互式表单系统。表单字段直接嵌入在 PDF 对象结构中:
<< /Type /Annot
/Subtype /Widget
/FT /Tx % 文本输入框
/T (username) % 字段名
/V (John Doe) % 当前值
/Rect [100 700 300 720] % 位置和大小
/DA (/Helvetica 12 Tf 0 g) % 默认外观
>>
支持的字段类型:
/Tx— 文本框(单行/多行)/Btn— 按钮(普通按钮/复选框/单选框)/Ch— 选择框(下拉/列表)/Sig— 数字签名框
AcroForm 的优势是简单且所有 PDF 阅读器都支持。缺点是布局能力有限——字段的位置和大小是固定的坐标值,不能响应式适配。
7.2.2 XFA:Adobe 的"重型"表单
**XFA(XML Forms Architecture)**是 Adobe 在 2003 年推出的另一套表单系统——比 AcroForm 强大得多,但也复杂得多。
XFA 用 XML 描述表单结构,支持:
- 动态布局(字段可以根据内容自动扩展)
- 复杂的数据绑定(从 XML 数据源填充表单)
- 子表单和重复区域(比如发票中的行项目可以动态添加)
- 条件逻辑(根据一个字段的值显示/隐藏其他字段)
但 XFA 的问题:
- 只有 Acrobat 完整支持:其他阅读器(包括 Chrome、Firefox、Sumatra)不支持或仅部分支持
- PDF 2.0 已弃用 XFA:ISO 在 PDF 2.0 规范中移除了 XFA,宣告其终结
- 安全性差:XFA 的复杂性带来了大量攻击面
尽管被弃用,大量政府和企业表单仍然使用 XFA 格式——因为它们是十年前创建的,没有人愿意花钱重做。
7.2.3 为什么政府至今离不开 PDF 表单
税务申报、签证申请、法院文件、医疗记录……全世界的官僚机构都依赖 PDF 表单。原因是:
- 格式固定:一份表单的布局不会因为接收端的软件版本而改变。政府需要标准化的纸面格式。
- 可打印:填完表单后可以直接打印,格式和空白表单完全对齐。
- 数字签名:PDF 支持符合法律效力的数字签名,很多国家的电子签名法认可 PDF 签名。
- 离线使用:不依赖网络连接,这在很多场景下仍然必要。
- 存档可靠:PDF/A 标准保证文件在几十年后仍能正确打开。
Web 表单(HTML form)虽然更现代、更友好,但在法律、存档和离线等维度上仍然无法完全替代 PDF 表单。
7.3 数字签名
7.3.1 PDF 签名的原理
PDF 数字签名的核心是 **PKCS#7(CMS)**格式的加密签名,嵌入在一个签名字段中:
签名流程:
1. 计算 PDF 文件特定字节范围的哈希(SHA-256)
2. 用签名者的私钥加密该哈希 → 得到签名值
3. 把签名值 + 签名者证书 + 时间戳嵌入 PDF
验证流程:
1. 用签名者的公钥(从证书中提取)解密签名值 → 得到原始哈希
2. 重新计算同一字节范围的哈希
3. 比较两个哈希——一致则签名有效
4. 验证证书链:签名者证书 → 中间 CA → 根 CA
5. 检查证书是否已被吊销(CRL/OCSP)
7.3.2 增量保存与签名的配合
还记得上一章讲的增量保存吗?它和数字签名配合得很好:
签名覆盖的字节范围是原文件内容(不包括签名值本身的占位空间)。之后的增量保存追加在文件末尾,不改变已签名的区域。这意味着:
- 签名后可以追加注释/填表:新内容在增量更新中,不影响签名有效性
- 修改已签名区域会使签名失效:阅读器会警告"文档在签名后被修改"
但这也带来了"Shadow Attack"风险——攻击者可以在签名前就埋入隐藏内容(通过增量保存覆盖显示),签名后再通过删除增量来"揭露"隐藏内容,而签名仍然显示有效。2020 年的研究表明主流 PDF 阅读器中有多个存在此类漏洞。
7.3.3 法律效力
不同国家/地区对 PDF 数字签名的法律认可程度不同:
- 欧盟 eIDAS 法规:认可"高级电子签名(AdES)"和"合格电子签名(QES)",PDF 签名可以满足
- 美国 ESIGN Act:广泛认可电子签名,但不要求特定技术格式
- 中国《电子签名法》:认可可靠的电子签名,PDF 签名在实践中被广泛使用
7.4 加密与权限控制
7.4.1 PDF 加密的两种密码
PDF 文件可以设置两种密码:
- User Password(用户密码):打开文件必须输入的密码。不知道密码就无法查看内容。
- Owner Password(所有者密码):控制权限的密码。没有它就不能打印、复制文字、编辑等。
权限控制的种类:
/P -3904 % 权限标志(位掩码)
| 权限位 | 控制内容 |
|---|---|
| bit 3 | 打印 |
| bit 4 | 修改内容 |
| bit 5 | 提取文字/图片 |
| bit 6 | 添加/修改注释和表单 |
| bit 9 | 填写表单 |
| bit 10 | 无障碍提取(屏幕阅读器) |
| bit 11 | 组装文档(插入/删除页面) |
| bit 12 | 高质量打印 |
7.4.2 加密算法的演进
| PDF 版本 | 算法 | 密钥长度 | 安全性(2026 年) |
|---|---|---|---|
| 1.1-1.3 | RC4 | 40 bit | ❌ 秒破 |
| 1.4 | RC4 | 128 bit | ⚠️ 已不推荐 |
| 1.5 | RC4 | 128 bit | ⚠️ 已不推荐 |
| 1.6 | AES | 128 bit | ✓ 尚可 |
| 1.7+ | AES | 256 bit | ✓ 安全 |
| 2.0 | AES-256 (ISO 32000-2) | 256 bit | ✓ 安全 |
重要的现实:Owner Password 只是一个"君子协定"。技术上,PDF 内容必须能被解密才能渲染——所以加密密钥实际上嵌入在文件中(用一定方式混淆)。有 Owner Password 保护但没有 User Password 的 PDF,其内容对任何有技术能力的人来说是完全可读的。很多开源工具(如 qpdf)可以直接去除 Owner Password 限制。
# 去除 PDF 的限制(如果没有 User Password)
qpdf --decrypt protected.pdf unlocked.pdf
User Password 则不同——它直接参与加密密钥的推导。不知道密码就真的无法解密内容(假设使用了 AES-256)。
7.5 PDF 安全攻击简史
7.5.1 PDF 作为攻击载体
PDF 是恶意软件最常用的载体之一。原因:
- 格式复杂:规范 1000+ 页,实现中必然有 bug
- 功能丰富:JS 引擎、字体解析、图片解码——每个都是独立的攻击面
- 用户信任:人们不像对 .exe 那样警惕 .pdf 文件
- 商业目标:企业和政府大量使用 PDF,攻击者的投入回报率高
7.5.2 经典攻击类型
1. JavaScript 攻击
// 恶意 PDF 中的 JS(简化示例)
var shellcode = unescape("%u9090%u9090...");
var spray = "";
while (spray.length < 0x40000) spray += shellcode;
// 触发堆溢出漏洞,跳转到 shellcode
通过 JS 精确控制内存布局(堆喷射),再触发渲染引擎的某个漏洞来获得代码执行。
2. 字体解析漏洞
字体文件(尤其是 Type 1 和 CFF)的解析器代码复杂且古老。畸形的字体数据可以触发缓冲区溢出。
3. 图片解码漏洞
JBIG2、JPEG 2000 等图片格式的解码器中的漏洞。2021 年 Apple 的 FORCEDENTRY 零点击漏洞就利用了 CoreGraphics 中的 JBIG2 解析器——恶意 PDF 通过 iMessage 发送,无需用户交互即可入侵 iPhone。
4. 行为攻击(无需漏洞)
- 自动打开嵌入的附件(如 .exe)
- 通过
/URI动作自动访问恶意 URL - 利用表单提交(
/SubmitForm)把数据外传 - 利用
/Launch动作尝试执行本地程序
7.5.3 防护措施
现代 PDF 阅读器的安全策略:
| 阅读器 | 策略 |
|---|---|
| Chrome PDFium | 运行在独立沙箱进程中,几乎无法逃逸 |
| Firefox PDF.js | JS 实现 → 无原生代码漏洞,天然安全 |
| Adobe Acrobat | Protected Mode 沙箱 + 禁止自动执行高危操作 |
| Sumatra | 不执行 JS、不支持表单提交、不加载外部资源 |
最安全的做法:用浏览器打开不信任的 PDF。浏览器的沙箱是经过十几年安全对抗锤炼出来的,比 PDF 阅读器的沙箱成熟得多。
7.6 PDF/A:为永久保存而设计
7.6.1 什么是 PDF/A
PDF/A 是 PDF 的存档子集(A = Archive)。它限制了 PDF 的功能,确保文件在几十年后仍然能被正确打开和渲染:
PDF/A 禁止的特性:
- ❌ JavaScript
- ❌ 外部引用(所有资源必须嵌入)
- ❌ 加密
- ❌ 音频/视频
- ❌ 透明度(PDF/A-1)或受限使用(PDF/A-2+)
- ❌ 非嵌入字体
PDF/A 要求的特性:
- ✓ 所有字体必须嵌入
- ✓ 必须包含 ICC 色彩配置文件
- ✓ 必须包含 XMP 元数据
- ✓ 必须声明 PDF/A 合规性标识
7.6.2 PDF/A 的级别
| 级别 | 基于 | 关键限制/改进 |
|---|---|---|
| PDF/A-1a | PDF 1.4 | 完整 Tagged + Unicode 映射 |
| PDF/A-1b | PDF 1.4 | 仅视觉保真(无结构要求) |
| PDF/A-2a/b/u | PDF 1.7 | 允许 JPEG 2000、透明度、层 |
| PDF/A-3a/b/u | PDF 1.7 | 允许嵌入任意文件附件 |
| PDF/A-4 | PDF 2.0 | 最新版本,简化合规级别 |
图书馆、档案馆、法院、医院——所有需要长期保存文档的机构都使用 PDF/A。德国法律要求电子发票以 PDF/A-3 格式存档(ZUGFeRD 标准)。