契约 / 安全原则
security-policy 领域包含三个默认拒绝(deny-by-default)的契约:CryptoPolicyInterface 管控算法与密钥选择,HtmlSecurityPolicyInterface 限制 HTML 功能范围,ExternalResourcePolicyInterface 管理远程资源加载。三者都是契约,因此部署端无需分叉代码,就能提供更严格的原则。
composer require nextpdf/core:^3概念总览
标题为“概念总览”的章节CryptoPolicyInterface 是加密的 gate(把关层)。在任何签名、加密或哈希步骤之前,核心都会先查询它。这项检查涵盖哈希、签名 OID、加密算法以及密钥强度。此契约还会返回最低哈希等级,以及供审计日志使用的原则名称。它会应用某一套规则,例如 FIPS 140-3 或 eIDAS。签名与加密代码本身保持不变。未设定任何原则时,所有算法都允许使用。受监管的网站必须明确设定一套原则。
HtmlSecurityPolicyInterface 作用于 HTML 解析层。它会在内容送达任何 renderer(渲染器)之前先执行。它会判定某个标签、属性、CSS 属性或 URL scheme 是否允许使用,也会限制输入大小与嵌套深度。它会与各 renderer 的传输原则(Chrome、Cloudflare、Gotenberg)配合,由后者设定大小上限与 CSP 标头。HTML 原则会缩小解析层的攻击面。被移除的标签永远不会进入布局阶段,因此注入的元素无法改变输出结果。未设定任何原则时,默认会允许完整的功能集。
ExternalResourcePolicyInterface 会判定 HTML 流水线是否可以获取外部字体、样式表或图像。它也会为每次获取设定上限。它的默认立场是全部拒绝(deny-all)。每个选项在你开启之前都是关闭的。此契约采用最小权限(least privilege)原则。不可信任的 HTML 可能指向攻击者的 URL。它会按 scheme、大小与字符数管控 @font-face 的获取。它会按 scheme、深度与总大小管控 @import。它会按 scheme 清单与完全匹配的域名允许清单管控 background-image。它会限制 data URI 的大小,也会管控 SVG 的外部引用。此契约声明:生产环境必须一律拒绝这些引用。它们会助长请求伪造与脚本注入。开放的 URL 获取是服务器端请求伪造(SSRF)的一条路径。根据 OWASP Top 10 2025,修改 URL 即可绕过访问控制。各项组件必须仅通过安全链接从官方来源取得。
API 接口
标题为“API 接口”的章节| 类型 | 种类 | 主要成员 | 稳定度 | 起始版本 |
|---|---|---|---|---|
CryptoPolicyInterface | 接口(interface) | isHashAlgorithmAllowed(), isSignatureAlgorithmAllowed(), isEncryptionAlgorithmAllowed(), isKeyStrengthAllowed(), getPreferredHashAlgorithm(), getName() | 稳定 | 1.9.0 |
HtmlSecurityPolicyInterface | 接口 | isTagAllowed(), isAttributeAllowed(), isCssPropertyAllowed(), isUrlSchemeAllowed(), getMaxInputSize(), getMaxNestingDepth(), getName() | 稳定 | 3.1.0 |
ExternalResourcePolicyInterface | 接口 | isFontFaceAllowed(), getAllowedFontSchemes(), getMaxFontFileSize(), getMaxFontGlyphs(), isImportAllowed(), getMaxImportDepth(), isBackgroundImageAllowed(), getAllowedImageDomains(), getMaxDataUrlSize(), isSvgExternalReferenceAllowed() | 稳定 | 4.0.0 |
ExternalResourcePolicyInterface 会返回带类型的边界值:positive-int 的大小、int<1, 100> 的 import 深度,以及 list<non-empty-string> 的 scheme 与域名清单。默认实现会拒绝每一项能力。
代码示例 — 快速开始
标题为“代码示例 — 快速开始”的章节<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\HtmlSecurityPolicyInterface;
/** * Decide whether a tag survives the policy. * * @param HtmlSecurityPolicyInterface $policy A core or custom policy. */function tagSurvives(HtmlSecurityPolicyInterface $policy, string $tag): bool{ return $policy->isTagAllowed($tag);}此函数依赖该契约。严格原则与默认原则都能满足此依赖。
代码示例 — 生产环境
标题为“代码示例 — 生产环境”的章节<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\CryptoPolicyInterface;use NextPDF\Contracts\ExternalResourcePolicyInterface;use NextPDF\Contracts\HtmlSecurityPolicyInterface;use Psr\Log\LoggerInterface;
final readonly class UntrustedHtmlGate{ public function __construct( private HtmlSecurityPolicyInterface $htmlPolicy, private ExternalResourcePolicyInterface $resourcePolicy, private CryptoPolicyInterface $cryptoPolicy, private LoggerInterface $logger, ) {}
/** * Reject input that exceeds the configured limits before rendering. * * @param string $html Untrusted HTML markup. */ public function assertAcceptable(string $html): void { $maxInput = $this->htmlPolicy->getMaxInputSize();
if ($maxInput > 0 && \strlen($html) > $maxInput) { $this->logger->warning('HTML rejected: input over limit', [ 'policy' => $this->htmlPolicy->getName(), 'limit' => $maxInput, ]);
throw new \LengthException('HTML input exceeds policy limit.'); }
if ($this->resourcePolicy->isSvgExternalReferenceAllowed()) { $this->logger->error('Unsafe policy: SVG external references enabled.');
throw new \LogicException('SVG external references must be denied in production.'); } }}此 gate 会在流水线执行之前,强制应用输入上限并拒绝不安全的资源原则。它会记录原则名称以供审计,并抛出特定异常。
边界情况与陷阱
标题为“边界情况与陷阱”的章节CryptoPolicyInterface在未设定任何原则时会允许所有算法。这个开放默认值是为了方便开发,并不代表生产环境的安全态势。任何受监管的部署都要明确设定一套原则。HtmlSecurityPolicyInterface::getMaxInputSize()返回0代表不限大小。请将0视为「原则不设限」,而不是「全部拒绝」,并同时在传输层应用一个上限。ExternalResourcePolicyInterface默认为全部拒绝。如果启用@font-face或background-image却不设定 scheme 清单,就会打开一个请求伪造攻击面;启用某项能力时,也要同时设定允许清单。- 如果
getAllowedImageDomains()的域名允许清单为空,则一旦启用背景图像,所有域名都会被允许。空清单并不等于拒绝;请明确提供域名。 isSvgExternalReferenceAllowed()在生产环境中应返回false。此契约已载明这一点;返回true的原则是一项审计发现,而不是一种配置选择。
一次原则检查就是一次谓词调用:O(1),不会产生与输入量成正比的成本。解析期间,会针对每个标签、每个属性、每个 CSS 属性与每个 URL 查询此原则。病态文件会放大调用次数。每次调用仍保持常数时间。performance_budget 设定的 1500 毫秒实际时间与 64 MB 峰值内存,主要由解析与渲染主导,而不是原则评估。输入大小与嵌套深度上限用于限制解析器本身的成本。严格原则会在布局之前拒绝过大或嵌套过深的文件,从而改善最差情况的性能。
安全注意事项
标题为“安全注意事项”的章节这些契约是引擎的防御边界,因此威胁模型有明确界定。算法降级由 CryptoPolicyInterface 缓解,它会在任何操作之前先阻止弱哈希与过短密钥。跨站脚本转 PDF 与内容注入由 HtmlSecurityPolicyInterface 缓解,它会在 renderer 执行之前,于解析层移除不允许的标签、属性与 CSS。服务器端请求伪造、解压缩炸弹与累积大小炸弹由 ExternalResourcePolicyInterface 缓解,它默认为全部拒绝,并按 scheme、大小、深度与域名限制每次获取。资源耗尽由输入大小、嵌套深度、字体字符数与 import 深度等上限缓解。由于每个原则都是契约,部署端无需分叉引擎就能强化边界,而原则名称也会公开供审计记录使用。请将所有 HTML、所有 URL,以及所有字体与图像字节都视为恶意。本页标记为 export_control_class: legal-review-required,因为这些契约规范了加密原则;本文全部以自有表述改写规范性来源,并未引用任何原文。
合规性
标题为“合规性”的章节| 声明 | 标准 | 条款 | 证据 |
|---|---|---|---|
| 未受约束的 URL 处理会让访问控制因 URL 修改而被绕过,外部资源原则会通过全部拒绝的默认值与完全匹配的域名允许清单缓解此问题。 | OWASP Top 10 2025 标准 | A01 | |
| 外部组件必须仅通过安全链接从官方来源取得,此原则会通过 scheme 允许清单落实这一点。 | OWASP Top 10 2025 标准 | 软件供应链 |
这两点均改写自 OWASP 指引。OWASP 数据以条款方式引注;引擎不会复制其文字。
商业场景
标题为“商业场景”的章节核心会定义并冻结这三个原则契约,并提供开发使用的宽松默认值,以及全部拒绝资源原则使用的严格默认值。Enterprise 版会在 CryptoPolicyInterface 后方提供一套 FIPS 140-3 配置文件,因此受监管的部署无需变更签名或加密代码,即可获得经过验证的算法态势。各版本的契约接口完全相同,差异在于部署端注入的原则实现。