セキュリティと運用
このブリッジは、ネットワーク境界を越えて HTML をブラウザエンジンへ送信します。このページでは、その境界を保護するすべての制御をソースに基づいて解説します。制御が標準を引用している場合、その引用はコード自体の docblock が宣言している内容です。このページはコード上の主張を言い換えたものであり、規範的な文言を再構成するものではありません。
脅威モデル
「脅威モデル」という見出しのセクションパッケージ自体の docblock には、防御対象とする脅威が明記されています。
- XSS-to-PDF — レンダリング中に実行される悪意あるマークアップ。
- SSRF — 内部アドレスへのリクエストを誘発するマークアップまたは宛先 URL。
- リソース枯渇 — 過大な入力や解凍爆弾。
- DNS リバインディング — 検証を通過したホスト名が、接続時にプライベートアドレスへ解決されること。
- 経路上の TLS 傍受 — Worker への経路上で差し替えられた証明書。
これらはいずれも、以下の具体的で検証可能な制御によって対処されています。
入力制御(リクエストが PHP を離れる前)
「入力制御(リクエストが PHP を離れる前)」という見出しのセクションCloudflareSecurityPolicy::validate() は、すべてのリクエストが構築される前に実行されます。
| 制御 | 動作 | 制限の出所 |
|---|---|---|
| サイズ上限 | maxHtmlSize を超える HTML を拒否 | CloudflareRendererConfig、デフォルトは 5000000 バイト |
| Base64 解凍爆弾ガード | すべての data:…;base64,… URI のデコード後サイズを見積もり、上限以上を拒否 | MAX_DATA_URI_BYTES = 13631488 |
| メタリフレッシュ禁止 | すべての <meta http-equiv="refresh"> を大文字小文字を区別せず拒否 | CloudflareSecurityPolicy 内の正規表現 |
違反時は、問題となった値と制限を示すメッセージとともに RuntimeException が送出されます。メタリフレッシュ禁止を設けているのは、refresh ディレクティブが Worker のレンダリングするページ内部からナビゲーションを誘発できるためです。これは URL ではなくコンテンツ内に潜む SSRF ベクトルです。
nextpdf/core の HTML セキュリティポリシー(HtmlSecurityPolicyInterface、デフォルトは DefaultHtmlSecurityPolicy)はパース層で動作し、上記のトランスポート層のチェックを補完します。getHtmlSecurityPolicy() で取得できます。カスタムポリシーはコンストラクター経由で注入します。
宛先制御(SSRF と DNS リバインディング)
「宛先制御(SSRF と DNS リバインディング)」という見出しのセクションCloudflareSecurityPolicy::validateWorkerUrl() は次を行います。
- パースできない URL、または scheme/host を欠く URL を拒否します(
Invalid Worker URL)。 - HTTPS 以外のスキームはすべて拒否します(
Worker URL must use HTTPS)。 - IP リテラルのホストは、PHP の
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGEを用いて、プライベートまたは予約済みの範囲を拒否します。実際には、これにより RFC 1918 のプライベート空間、ループバック、RFC 3927 のリンクローカルアドレスが拒否されます。テストは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 が指定されている場合、レンダラーは注入された PSR-18 クライアントの代わりに Transport\PinnedCurlTransport を使用します。このトランスポートは、cURL ハンドル層で次の制御を強制します。
- ピン留めされた 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 エラー、または文字列以外のボディが返された場合は、cURL のエラー番号とメッセージを伴って CloudflareRenderException が送出されます。
ピンニングの運用ガイダンス
「ピンニングの運用ガイダンス」という見出しのセクションこの設定には、pinnedPublicKeys と、別個の backupPublicKeys が含まれます。RFC 7469 §2.5 は、バックアップピン(オフラインで保管された、まだデプロイされていない二次的な鍵ペアのフィンガープリント)を、不注意によるピン検証の失敗から回復するための主要な手段として説明しています。証明書のローテーションでエンドポイントが使用不能にならないよう、少なくとも 1 つのバックアップピンを保持することは、このガイダンスに沿っています。別個のフィールドにすることで、ローテーションを独立して検証できます。運用上は次のとおりです。
- リーフ証明書、または自分でローテーションを管理している中間証明書の SPKI をピン留めします。
- ローテーション前に、必ず次の証明書のバックアップピンを設定します。
- 空のピンセットはピンニングを無効化します。これは安定した既知の証明書チェーンがある場合にのみ使用します。ピンニングは設定によるオプトインです。
認証とシークレットの取り扱い
「認証とシークレットの取り扱い」という見出しのセクション- Worker へのリクエストには
Authorization: Bearer <apiToken>が付与されます。apiTokenは#[SensitiveParameter]であるため、スタックトレースから秘匿されます。到達性プローブは、HTTPHEADで同じベアラーヘッダーを送信します。 - 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()の不使用、SHA に固定された GitHub Actions です。
このパッケージが主張しないこと
「このパッケージが主張しないこと」という見出しのセクション- Cloudflare プラットフォームの制限(Worker の CPU 時間、メモリ、リクエストボディの上限、サブリクエスト数)については何も主張しません。このドキュメントが述べるサイズと時間の制限は、パッケージ自体が強制するものだけであり、上記および /integrations/cloudflare/configuration/. に列挙されています。プラットフォームの制限については、Cloudflare の公式ドキュメントと、お使いの Worker 自体の実装を参照してください。
- PDF への署名は行わず、署名適合性に関する主張もしません。署名が必要な場合は、ここでレンダリングしたうえで、エンジンで署名してください。NextPDF Pro が提供するのは PAdES B-B 署名のみです。長期検証プロファイルは Enterprise の機能であり、このブリッジの対象範囲外です。
- パイプラインが「改ざん不可能」であると認証、保証、または立証するものではありません。このページに記載された、具体的でソースから検証可能な制御を実装するのみであり、それを超えるものは実装しません。
運用ランブック
「運用ランブック」という見出しのセクション| 症状 | 最初に確認すること |
|---|---|
Worker URL must use HTTPS | 設定された workerUrl のスキーム。 |
private or reserved IP | Worker ホスト名の DNS レコード。RFC 1918 / ループバック / RFC 3927 空間へ解決されるレコード。 |
DNS answer changed since validation | DNS の不安定さ、またはリバインディングの試み。再解決してレコードセットを確認します。 |
cURL transport error | ネットワーク経路、TLS チェーン、ピンが設定されている場合は提示された証明書の SPKI がまだピンセットに含まれているかどうか。 |
| 証明書ローテーションの直後にレンダリングが失敗する | 一致するバックアップピンのないピンセット。ローテーションの前に、新しい SPKI をバックアップとして追加します。 |
is not installed / no LocalRendererFactoryInterface | フォールバックが有効でもファクトリーが接続されていない状態、または nextpdf/artisan が存在しない状態。 |
| レート制限の拒否がノード間で一貫しない | インメモリのリミッターはプロセス単位です。共有ストアを前段に配置します。 |
インシデントの報告
「インシデントの報告」という見出しのセクション脆弱性は、GitHub Security Advisories、またはリポジトリの SECURITY.md に記載のセキュリティ連絡先を通じて報告してください。セキュリティに関する問題を、公開の GitHub Issue として登録しないでください。
- /integrations/cloudflare/overview/ — このパッケージがこの境界を中心に設計されている理由。
- /integrations/cloudflare/configuration/ — ピンセットと制限のフィールド。
- /integrations/cloudflare/troubleshooting/ — 失敗から例外への完全な対応表。