安全与运维
该桥接会把你的 HTML 跨越网络边界发送到浏览器引擎。本页基于源代码,记录保护该边界的每一项控制措施。某项控制措施引用标准时,该引用即来自代码自身 docblock 的声明。本页仅重述代码的主张,不会重新表述规范性文本。
威胁模型
标题为“威胁模型”的章节此软件包自身的 docblock 列出了它所防御的威胁:
- XSS 转 PDF — 在渲染期间运行的恶意标记。
- SSRF — 驱使请求送往内部地址的标记或目标 URL。
- 资源耗尽 — 过大的输入或解压缩炸弹。
- DNS 重新绑定 — 通过验证的主机名称,却在连接时解析到私有地址。
- 路径上 TLS 拦截 — 在通往 Worker 的路径上被替换的证书。
以下每项都由具体且可测试的控制措施处理。
输入控制(在请求离开 PHP 之前)
标题为“输入控制(在请求离开 PHP 之前)”的章节CloudflareSecurityPolicy::validate() 会在构建任何请求之前运行:
| 控制措施 | 行为 | 限制来源 |
|---|---|---|
| 大小上限 | 拒绝大于 maxHtmlSize 的 HTML | CloudflareRendererConfig,默认 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() 获取它。可通过构造函数注入自定义策略。
目标控制(SSRF 与 DNS 重新绑定)
标题为“目标控制(SSRF 与 DNS 重新绑定)”的章节CloudflareSecurityPolicy::validateWorkerUrl():
- 拒绝无法解析或缺少 scheme/host 的 URL(
Invalid Worker URL)。 - 拒绝任何非 HTTPS 的 scheme(
Worker URL must use HTTPS)。 - 对于 IP 字面量主机,会使用
PHP 的
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE拒绝私有或保留范围。实际效果是拒绝 RFC 1918 私有空间、loopback,以及 RFC 3927 link-local 地址 — 测试明确覆盖了对192.168.x、127.0.0.1与169.254.x的拒绝。范围归属由 PHP 的 filter 扩展决定,而非由此软件包固定到某个条款;此处提及 RFC 1918 与 RFC 3927 仅是描述性地引用这些范围的常见定义。 - 对于主机名称,会通过
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)窗口。
传输控制(PinnedCurlTransport)
标题为“传输控制(PinnedCurlTransport)”的章节当存在已审核的 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 => true、CURLOPT_SSL_VERIFYHOST => 2。 - 不自动重定向 —
CURLOPT_FOLLOWLOCATION => false、CURLOPT_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],因此会从堆栈追踪中屏蔽。可达性探测会在 HTTPHEAD请求上发送相同的 Bearer 标头。 - R2 访问密钥(
accessKeyId、secretAccessKey)标注为#[SensitiveParameter],并且只用于派生 AWS Signature V4 签署密钥。 ApiKeyValidator以hash_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 IP | Worker 主机名称的 DNS 记录;某条记录解析到 RFC 1918/loopback/RFC 3927 空间。 |
DNS answer changed since validation | DNS 不稳定或重新绑定尝试;重新解析并查看记录集合。 |
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/ — 完整的失败到异常映射。