跳到內容

安全性與維運

此橋接會將你的 HTML 跨越網路邊界送往瀏覽器引擎。本頁依據原始碼,記錄防護該邊界的每一項控制措施。當某項控制措施引用標準時,該引用即為程式碼本身 docblock 所宣告的內容。本頁僅重述程式碼的主張,不會重建規範性措辭。

此套件本身的 docblock 列出它所防禦的威脅:

  • XSS 轉 PDF — 在算繪期間執行的惡意標記內容。
  • SSRF — 驅使請求送往內部位址的標記或目標 URL。
  • 資源耗盡 — 過大的輸入或解壓縮炸彈。
  • DNS 重新繫結 — 已通過驗證的主機名稱,卻在連線時解析到私有位址。
  • 路徑上 TLS 攔截 — 通往 Worker 的路徑上遭替換的憑證。

以下每一項都由具體且可測試的控制措施處理。

CloudflareSecurityPolicy::validate() 會在建構任何請求前執行:

控制措施行為限制來源
大小上限拒絕大於 maxHtmlSize 的 HTMLCloudflareRendererConfig,預設 5000000 位元組
Base64 解壓縮炸彈防護估算每一個 data:…;base64,… URI 的解碼後大小;at/above 上限即拒絕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 層強制執行:

  • 釘選 DNSCURLOPT_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,或你能掌控其輪替的中介憑證。
  • 在輪替之前,務必為下一張憑證設定備援釘選。
  • 空的釘選集合會停用釘選;僅在憑證鏈穩定且已知時才這麼做。釘選是依設定選擇性啟用。
  • 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/ — 完整的失敗對例外映射。