PAYATHON 2026

嵌入不可信 PDF 预览时,哪种方式最安全?

技术老齐

结论

处理不可信 PDF 时,可以按以下优先级选择方案:

  1. 安全要求最高时,在隔离的服务端环境中把 PDF 转成图片,再由浏览器展示。
  2. 需要完整 PDF 功能时,在独立来源中运行受控版本的 PDF.js,并通过带 sandbox 的 <iframe> 嵌入。
  3. 避免直接用 <object> 或 <embed> 加载不可信 PDF。这两个元素没有 sandbox 属性,应用无法细粒度限制导航、脚本、下载和弹窗等能力。

iframe sandbox 可以限制 PDF 查看器遭到攻击后的影响范围,但无法修复 PDF.js、浏览器内置查看器或底层图形库中的漏洞。因此,渲染器需要及时更新,服务端渲染也要放在受限进程或容器中运行。

推荐架构

PDF 文件和查看器应与主应用使用不同来源,例如:

https://app.example.com          主应用
https://preview.example.net      PDF.js 查看器和 PDF 文件

预览来源应满足以下条件:

  • 不承载登录、管理后台等敏感页面。
  • 不接收主应用的会话 Cookie。
  • 不共享 localStorage、Service Worker 或认证令牌。
  • 只提供查看器和短期有效的文件访问地址。
  • 限制文件大小、页数、解压后体积和渲染时间。
  • 使用仍在维护的 PDF.js 版本,并及时安装安全更新。

条件允许时,预览服务应部署在完全独立的站点,而非主站的子域名,以减少父域 Cookie 和其他共享配置带来的意外影响。

<iframe> 的最小权限

PDF.js 需要执行 JavaScript,因此通常至少要授予:

sandbox="allow-scripts"

采用这个配置时,iframe 使用不透明来源,隔离效果较强。PDF.js 在发起 fetch、使用 Worker 或读取同源资源时,可能受到 CORS 和存储限制。如果查看器能正常工作,就不必继续增加权限。

更常见的部署方式是把 PDF.js 和 PDF 文件放在同一个独立预览来源中,然后使用:

<iframe
  src="https://preview.example.net/viewer.html?document=OPAQUE_DOCUMENT_ID"
  sandbox="allow-scripts allow-same-origin"
  referrerpolicy="no-referrer"
  loading="lazy"
  title="PDF 预览"
></iframe>

allow-same-origin 会让查看器保留 preview.example.net 来源,方便加载 PDF、Worker 和字体。只有当预览站点与主应用确实跨来源时,才适合使用这个权限。

不要在主应用的同源页面上同时授予 allow-scripts 和 allow-same-origin。同源脚本可能访问父页面中的 iframe 元素并移除 sandbox,导致隔离失效。

不应默认授予的权限

正常渲染 PDF 通常不需要这些权限:

  • allow-top-navigation
  • allow-top-navigation-by-user-activation
  • allow-popups
  • allow-popups-to-escape-sandbox
  • allow-forms
  • allow-modals
  • allow-downloads

allow-top-navigation-by-user-activation 允许查看器在用户操作后跳转顶层页面,而 PDF.js 渲染并不依赖这个权限。

只有确实需要提供“下载原文件”功能时,才考虑加入 allow-downloads:

sandbox="allow-scripts allow-same-origin allow-downloads"

也可以由主应用在 iframe 外提供下载按钮。这样无须扩大 iframe 权限,权限校验、审计和限速也更容易处理。

配合 CSP 限制加载范围

主应用可以通过 CSP 禁止 <object> 和 <embed>,并把 iframe 限制在指定的预览来源:

Content-Security-Policy:
  default-src 'self';
  object-src 'none';
  frame-src https://preview.example.net;

预览站点也需要配置自己的 CSP。下面的配置可作为起点,实际指令应根据 PDF.js 的构建方式调整:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  connect-src 'self';
  img-src 'self' blob: data:;
  worker-src 'self' blob:;
  object-src 'none';
  base-uri 'none';
  form-action 'none';
  frame-ancestors https://app.example.com;

如果预览服务使用 CDN、WebAssembly、字体文件或其他来源的 Worker,应逐项添加允许规则,避免直接放宽到任意来源。

PDF 响应至少要使用正确的媒体类型,并阻止 MIME 嗅探:

Content-Type: application/pdf
X-Content-Type-Options: nosniff

文件标识应使用服务端生成的不可猜测 ID 或短期签名令牌。用户提供的文件路径不能直接拼接到查看器 URL 中。

为什么不推荐 <object> 和 <embed>

浏览器是否通过内置 PDF 查看器、插件或下载流程处理 <object> 和 <embed>,取决于浏览器和用户配置。应用难以获得一致的安全边界,也无法通过元素属性施加 sandbox 限制。

例如:

<object data="/uploads/untrusted.pdf" type="application/pdf"></object>

这段代码可以显示 PDF,却无法限制查看器的导航、弹窗和下载能力。浏览器通常还有进程级沙箱,但这是浏览器自身的实现,无法替代网站可配置的 iframe 沙箱。

下面的写法也不代表使用了 PDF.js:

<iframe src="/uploads/untrusted.pdf" sandbox></iframe>

浏览器可能仍会调用内置查看器,而且各浏览器对沙箱内 PDF 的支持并不一致。如果需要稳定且安全策略可控的预览,应明确部署自己的查看器页面。

安全性要求更高时使用图片预览

如果业务只要求用户快速查看内容,不需要搜索、复制文本、填写表单或使用 PDF 内部链接,可以在服务端把每一页转换为 PNG、JPEG 或 WebP:

<img
  src="https://preview.example.net/documents/abc/pages/1.webp"
  alt="PDF 第 1 页"
/>

转换程序同样需要解析不可信 PDF,因此必须在隔离环境中运行,并设置以下限制:

  • 限制 CPU、内存和执行时间。
  • 限制文件大小和页数。
  • 禁止访问外网。
  • 使用只读文件系统和临时输出目录。
  • 使用非特权用户运行。
  • 转换完成后重新编码图片,不要原样转发 PDF 中嵌入的资源。

这种方案可以避免浏览器直接解析 PDF,但解析风险会转移到服务端转换器,所以不能在没有隔离的主应用进程中执行。

还需要防范的风险

PDF 中的 JavaScript 只是其中一种风险。查看器漏洞、恶意字体、压缩炸弹、超大页面、异常图片、外部链接和社会工程内容也需要纳入防护范围。

上传时检查扩展名和 MIME 类型,或进行恶意软件扫描,可以过滤部分文件,但不能据此认定 PDF 安全。简单删除 /JavaScript、/OpenAction 等字段也无法可靠地“净化”PDF。文件结构、对象流和增量更新都可能让这种处理遗漏内容。

通用场景可以采用独立预览来源中的 PDF.js,通过最小权限的 sandboxed iframe 加载,同时配置严格的 CSP,并使用不含敏感凭据的文件访问机制。隔离要求更高且功能需求较少时,可以改用服务端受限渲染后的图片预览。

备注:内容仅供参考。