安全性與維運
此橋接會將你的 HTML 跨越網路邊界送往瀏覽器引擎。本頁依據原始碼,記錄防護該邊界的每一項控制措施。當某項控制措施引用標準時,該引用即為程式碼本身 docblock 所宣告的內容。本頁僅重述程式碼的主張,不會重建規範性措辭。
威脅模型
標題為「威脅模型」的區段此套件本身的 docblock 列出它所防禦的威脅:
- XSS 轉 PDF — 在算繪期間執行的惡意標記內容。
- SSRF — 驅使請求送往內部位址的標記或目標 URL。
- 資源耗盡 — 過大的輸入或解壓縮炸彈。
- DNS 重新繫結 — 已通過驗證的主機名稱,卻在連線時解析到私有位址。
- 路徑上 TLS 攔截 — 通往 Worker 的路徑上遭替換的憑證。
以下每一項都由具體且可測試的控制措施處理。
輸入控制(在請求離開 PHP 之前)
標題為「輸入控制(在請求離開 PHP 之前)」的區段CloudflareSecurityPolicy::validate() 會在建構任何請求前執行:
| 控制措施 | 行為 | 限制來源 |
|---|---|---|
| 大小上限 | 拒絕大於 maxHtmlSize 的 HTML | CloudflareRendererConfig,預設 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() 取得,也可透過建構式注入自訂政策。
目標控制(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,或你能掌控其輪替的中介憑證。
- 在輪替之前,務必為下一張憑證設定備援釘選。
- 空的釘選集合會停用釘選;僅在憑證鏈穩定且已知時才這麼做。釘選是依設定選擇性啟用。
身分驗證與密鑰處理
標題為「身分驗證與密鑰處理」的區段- 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/ — 完整的失敗對例外映射。