跳转到内容

安全与运维

该桥接会把你的 HTML 跨越网络边界发送到浏览器引擎。本页基于源代码,记录保护该边界的每一项控制措施。某项控制措施引用标准时,该引用即来自代码自身 docblock 的声明。本页仅重述代码的主张,不会重新表述规范性文本。

此软件包自身的 docblock 列出了它所防御的威胁:

  • XSS 转 PDF — 在渲染期间运行的恶意标记。
  • SSRF — 驱使请求送往内部地址的标记或目标 URL。
  • 资源耗尽 — 过大的输入或解压缩炸弹。
  • DNS 重新绑定 — 通过验证的主机名称,却在连接时解析到私有地址。
  • 路径上 TLS 拦截 — 在通往 Worker 的路径上被替换的证书。

以下每项都由具体且可测试的控制措施处理。

CloudflareSecurityPolicy::validate() 会在构建任何请求之前运行:

控制措施行为限制来源
大小上限拒绝大于 maxHtmlSize 的 HTMLCloudflareRendererConfig,默认 5000000 字节
Base64 解压缩炸弹防护估算每一个 data:…;base64,… URI 的解码后大小;达到或超过上限即拒绝MAX_DATA_URI_BYTES = 13631488
Meta-refresh 禁用拒绝任何 <meta http-equiv="refresh">,不分大小写CloudflareSecurityPolicy 中的正则表达式

违反规则时会引发 RuntimeException,消息会指明违规值和限制。meta-refresh 禁用之所以存在,是因为 refresh 指令可以从 Worker 渲染的页面内部触发导航 — 这是一个位于内容(而非 URL)中的 SSRF 攻击向量。

来自 nextpdf/core 的 HTML 安全策略(HtmlSecurityPolicyInterface,默认为 DefaultHtmlSecurityPolicy)作用于解析层,与上述传输层检查互补。可通过 getHtmlSecurityPolicy() 获取它。可通过构造函数注入自定义策略。

CloudflareSecurityPolicy::validateWorkerUrl()

  1. 拒绝无法解析或缺少 scheme/host 的 URL(Invalid Worker URL)。
  2. 拒绝任何非 HTTPS 的 scheme(Worker URL must use HTTPS)。
  3. 对于 IP 字面量主机,会使用 PHP 的 FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE 拒绝私有或保留范围。实际效果是拒绝 RFC 1918 私有空间、loopback,以及 RFC 3927 link-local 地址 — 测试明确覆盖了对 192.168.x127.0.0.1169.254.x 的拒绝。范围归属由 PHP 的 filter 扩展决定,而非由此软件包固定到某个条款;此处提及 RFC 1918 与 RFC 3927 仅是描述性地引用这些范围的常见定义。
  4. 对于主机名称,会通过 dns_get_record() 解析所有 A 与 AAAA 记录(而非 gethostbyname(),后者只返回第一条答案),并在任何一个解析出的地址为私有或保留时拒绝。

采用全记录解析是刻意的设计,并记载于类 docblock 中,用以防御会返回多条记录的主机:单条记录查询可能选中公开地址,而后续连接却选中私有地址。这与 OWASP SSRF Prevention Cheat Sheet 一致,后者指示应用程序取得域名背后的所有 IP 地址(A 与 AAAA 记录),并对其中每一个应用非公开地址检查。

validateWorkerUrl() 会返回已审核的 IP 集合。随后渲染器会在发送前立即调用 assertPinsStillValid()。该调用会重新解析主机,并在验证后出现新 IP 时拒绝(Worker URL DNS answer changed since validation — possible DNS rebinding attack)。这会封闭验证与连接之间的检查时点/使用时点(time-of-check / time-of-use)窗口。

当存在已审核的 IP 集合或 SPKI 固定集合,提供了 PSR-17 的 ResponseFactory 时,渲染器会改用 Transport\PinnedCurlTransport,而非注入的 PSR-18 客户端。此传输会在 cURL handle 层强制执行:

  • DNS 固定CURLOPT_RESOLVE 会将 host:port 绑定到已审核的 IP 集合,因此 libcurl 不会在连接时自行查询。这正是让用户空间的 DNS 检查确实绑定连接的关键;没有它,libcurl 可能会解析到不同的地址。
  • TLS 公开密钥固定CURLOPT_PINNEDPUBLICKEY 会以合并后的固定集合设置。这遵循 RFC 7469 §2.6:当服务器提供的 SPKI 指纹集合与已配置的固定集合有交集时,即接受该固定连接,而固定验证失败会被视为不可恢复。固定字符串会从 sha256/<base64> 范式转换为 cURL 的 sha256//<base64> 形式;格式错误的固定值会引发 InvalidSpkiPinException
  • TLS 验证已开启CURLOPT_SSL_VERIFYPEER => trueCURLOPT_SSL_VERIFYHOST => 2
  • 不自动重定向CURLOPT_FOLLOWLOCATION => falseCURLOPT_MAXREDIRS => 0。3xx 会交给策略层,而非由 libcurl 跟随到未经审核的主机。类 docblock 指出这是刻意的选择,使重定向可以重新验证,而不是被静默跟随。
  • 硬性超时CURLOPT_TIMEOUT 会依 renderTimeout 设置(默认 30 秒)。

cURL 错误或非字符串的响应主体会引发 CloudflareRenderException,并带有 cURL 错误代码与消息。

此配置包含 pinnedPublicKeys,以及另一个独立的 backupPublicKeys。RFC 7469 §2.5 将备用固定值描述为从非预期的固定验证失败中恢复的主要方式 — 它是一个离线保存的次要、尚未部署密钥对的指纹。保留至少一个备用固定值,避免证书轮换导致端点失效,这正是对该指引的遵循。这个独立字段让轮换可以单独验证。运维上:

  • 固定叶证书的 SPKI,或固定你能掌控轮换的中间证书的 SPKI。
  • 在轮换之前,务必为下一张证书配置备用固定值。
  • 空的公钥固定集合会停用公钥固定;只有在证书链稳定且已知时才这么做。公钥固定会根据配置选择性启用。
  • Worker 请求会带有 Authorization: Bearer <apiToken>apiToken 标注为 #[SensitiveParameter],因此会从堆栈追踪中屏蔽。可达性探测会在 HTTP HEAD 请求上发送相同的 Bearer 标头。
  • R2 访问密钥(accessKeyIdsecretAccessKey)标注为 #[SensitiveParameter],并且只用于派生 AWS Signature V4 签署密钥。
  • ApiKeyValidatorhash_equals()(时序安全)比较密钥,并通过 validateHashed() 支持 SHA-256 哈希密钥存储。
  • 配置对象为 final readonly — 密钥一旦设置便无法再变更。
  • 从环境变量或密钥管理系统获取密钥。绝不要将其提交到版本控制。此软件包遵循更广泛的 NextPDF 安全基准:PHPStan Level 10、每个文件都有 declare(strict_types=1)、不使用 eval()/exec(),GitHub Actions 固定到 SHA。
  • 它不声明任何 Cloudflare 平台限制(Worker CPU 时间、内存、请求主体上限或子请求数量)。本文档声明的大小和时间限制仅限此软件包自身强制执行的限制,见上文以及 /integrations/cloudflare/configuration/.。至于平台限制,请参阅 Cloudflare 的官方文档以及你自己 Worker 的实现。
  • 它不签署 PDF,也不对签名合规性作出任何主张。需要签名时,请先在此处渲染,再以引擎签署。NextPDF Pro 仅提供 PAdES B-B 签署;长期验证配置文件属于 Enterprise 功能,不在此桥接层范围内。
  • 它不认证、不担保,也不会将管线渲染为「防篡改」。它只实现本页所述、可由源代码验证的特定控制措施,仅此而已。
现象优先检查
Worker URL must use HTTPS已配置的 workerUrl scheme。
private or reserved IPWorker 主机名称的 DNS 记录;某条记录解析到 RFC 1918/loopback/RFC 3927 空间。
DNS answer changed since validationDNS 不稳定或重新绑定尝试;重新解析并查看记录集合。
cURL transport error网络路径、TLS 证书链,以及 — 若已配置固定 — 所提供证书的 SPKI 是否仍在固定集合中。
证书轮换后立即渲染失败固定集合中没有匹配的备用固定值。在轮换之前先将新的 SPKI 加为备用固定值。
is not installed / no LocalRendererFactoryInterface启用了后备但未接入工厂,或缺少 nextpdf/artisan
跨节点的速率限制拒绝不一致内存限流器按进程(per-process)生效;请在其前面加上共享存储。

请通过 GitHub Security Advisories,或仓库 SECURITY.md 中的安全联系方式报告漏洞。请勿以公开的 GitHub issue 形式提交安全议题。

  • /integrations/cloudflare/overview/ — 为什么此软件包围绕这个边界设计。
  • /integrations/cloudflare/configuration/ — 固定集合与限制字段。
  • /integrations/cloudflare/troubleshooting/ — 完整的失败到异常映射。