跳到內容

契約 / 安全性原則

security-policy 領域包含三個預設拒絕(deny-by-default)的契約:CryptoPolicyInterface 管控演算法與金鑰選擇,HtmlSecurityPolicyInterface 限制 HTML 可使用的功能範圍,ExternalResourcePolicyInterface 則控管遠端資源載入。每一項都是契約,因此部署端無須分叉程式碼,也能提供更嚴格的原則。

Terminal window
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 即可繞過存取控制。各項元件必須僅透過安全連結,從官方來源取得。

型別種類主要成員穩定度起始版本
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 與網域清單。預設實作會拒絕每一項能力。

examples/contracts/security-policy-quickstart.php
<?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);
}

此函式相依於這個契約;嚴格原則與預設原則都能滿足它。

examples/contracts/security-policy-production.php
<?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-facebackground-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 設定檔,因此受監管的部署無須變更簽署或加密程式碼,即可獲得經驗證的演算法姿態。各版本的契約介面完全相同,差異在於部署端注入的原則實作。