コンテンツにスキップ
getnextpdf.com

信頼できない PDF のサニタイズ — コンテンツ無害化と再構築

Spec: ISO 32000-2, §12.6.4

外の世界から届く PDF は、文書であると同時にコードでもあります。それは JavaScript、Launch アクション、埋め込み実行ファイル、そしてパーサーをつまずかせるよう細工された構造を携え得ます。Content Disarm & Reconstruction(CDR)は、そのファイルを信頼できないものとして扱い、安全なものだけを残し、生き残ったものからクリーンな PDF を再構築します。

このページは、NextPDF Enterprise の CdrEngine がそれをどう行うか — そして、すべての CDR ベンダーが声に出して言うべき 1 つの正直なこと — を説明します。再構築されたファイルは、元のファイルの セキュリティ投影 であって、その忠実なコピーではありません。

PDF の危険な部分は、風変わりなものではありません。それらは標準的な機能です。仕様は アクション のまるごとのカタログ — 文書が開くとき、ページが表示されるとき、フィールドが変わるときに何が起きるか — を定義し、そのカタログには JavaScript と Launch アクションが含まれます(Spec: ISO 32000-2, §12.6.4)。フォーマットを尊重するビューアは、喜んでそれらを実行します。それが攻撃者の足がかりです。完璧に有効で、同時に完璧に悪意あるファイルです。

ファイル拡張子によるフィルタリングは、ここでは何もしません。脅威は整形式の PDF の内側にあるので、唯一の本物の防御は、それを開き、理解し、レンダラーに到達する前に能動的な機械装置を剥がすことです。これは、OWASP のファイルアップロードと入力検証の指針が記述する入力検証の姿勢です。バイトを決して信頼せず、敵対的なものを既知の悪のシグネチャでスキャンするより、既知の良好なアーティファクトを再構築することを選ぶ、です。

  • CDR は入力が敵対的であると仮定し、古いものにパッチを当てるのではなく 新しい ファイルを生成します。
  • CdrEngine::sanitize() は 6 つのフェーズを実行します。パース、受け入れ制御、脅威検出、フィルタ、参照スクラブ、再構築です。
  • それは CdrResult を返し、何が除去されたか、文書がそもそも 受け入れられた かどうか、そして — そうでなければ — なぜ拒否されたかを告げます。
  • 出力は セキュリティ投影 です。それは明示的に、証拠としてのコピー、ハッシュ一致、あるいはアーカイブアーティファクトでは ありません。それは但し書きではなく契約です。
  • これは Enterprise のみです。NextPDF core は CDR を行いません。

エンジン自身のドキュメントは、設計上の越えてはならない一線を 3 語で述べます。Security Projection Layer です。sanitize() は破壊的で非可逆な変換です。バイトを捨てることは許されています。許されていないのは、結果が同じ文書であるふりをすることです。

パイプラインは意図的に順序付けられています。各フェーズは、次のフェーズがそれに作用する前に信頼を絞ります。

  1. ParsePdfReader builds the object graph from the raw bytes. A parse failure is a rejection, not a best-effort guess.
  2. Admission controlResource limits are checked first — object count, page count, decoded-stream size, per-stream inflation ratio. Over-budget input is rejected, never sanitised.
  3. DetectThreatDetector walks the graph and records each dangerous feature as a DetectedThreat with its object number and type.
  4. FilterObjects are partitioned into safe and removed. Catalog-level keys are stripped in place so the document catalog itself survives.
  5. Reference scrubPointers to removed objects are cleaned so the rebuilt file has no dangling references.
  6. RebuildCdrRebuilder serialises only the safe objects into a new PDF. The output is fresh structure, not an edited original.
The six phases of CdrEngine::sanitize(), in order. The document is parsed, then admitted or rejected against resource limits, then scanned for threats; dangerous objects are filtered out, dangling references are scrubbed, and only the surviving safe objects are serialised into a brand-new PDF. The original bytes never carry through.

無害化の前に受け入れを。 単一の脅威が除去される前に、エンジンは文書がそもそも処理する価値があるかを尋ねます。CdrPolicy が制限 — maxObjectsmaxPageCountmaxDecodedStreamBytes、そして解凍爆弾を防ぐ maxInflationRatio — を携えます。それらを大きく超える文書は 拒否 され、CdrResultadmitted: falserejectionReason でそれを告げます。この区別は荷重を担います。「私たちはそれをクリーンにした」と「私たちはそれを拒んだ」は異なる結果であり、結果型はそれらを分けて保つので、あなたのエラー報告もそうできます。

脅威は名付けられ、推測されません。 ThreatDetector::detect() は各危険な構造を ThreatType へ対応づけます — JavaScriptLaunchActionOpenActionAdditionalActionsRemoteGoToSubmitFormImportDataEmbeddedFilesRichMediaNamedJavaScript、その他 — それぞれが特定の PDF 機能に結びつきます。まったくパースできないオブジェクトは、それ自身の脅威型 UnparseableObject になります。サニタイザーが読めないオブジェクトは、保証できないオブジェクトだからです。すべての発見事項は、問題のオブジェクト番号を携える DetectedThreat なので、除去は精密です。

除去はペイロードだけでなく文書を守ります。 いくつかの危険なキーは文書カタログ(Root)に存在します — たとえば開くときに発火する /OpenAction です。1 つのキーを殺すために Root オブジェクト全体を除去すれば、カタログを破壊し、ファイルをひそかに壊すことになります。エンジンは代わりにこれらをカタログ上の その場でのキー剥がし として扱い、フェイルクローズのガードとして、/Root を完全に失った再構築済み文書の送出をそもそも拒みます。成功を報告しながら構造的に壊れたファイルを生成するサニタイザーこそ、このガードが防ぐために存在する、まさにその失敗モードです。

下記の形は本物のエントリポイントです。エンジンに生のバイトとポリシーを手渡し、何が起きたかについて正直な CdrResult を受け取ります。

<?php
declare(strict_types=1);
use NextPDF\Enterprise\Security\Cdr\CdrEngine;
use NextPDF\Enterprise\Security\Cdr\CdrPolicy;
$engine = new CdrEngine();
// Standard policy removes the known active threats — JavaScript, Launch
// actions, remote go-to, form submit/import, and the rest — while leaving
// the lossy opt-in "Strip*" cases off by default.
$result = $engine->sanitize($untrustedPdfBytes, CdrPolicy::standard());
if (!$result->admitted) {
// Rejected by admission control (e.g. object/page limit, zip bomb).
// This is NOT a sanitised document. Do not serve it; report the reason.
throw new \RuntimeException($result->rejectionReason);
}
if ($result->hadThreats()) {
// The disarmed bytes are safe to render. Each removed threat carries its
// type and object number for your audit log — never silently.
foreach ($result->removedThreats as $threat) {
error_log(\sprintf(
'CDR removed %s in object %d',
$threat->type->value,
$threat->objectNumber,
));
}
}
$cleanBytes = $result->sanitizedPdf; // The security projection. Not the original.

このコードがなお危険なファイルをひそかに返す経路はありません。文書は受け入れられて無害化されるか、述べられた理由で拒否されるか、のいずれかです。removedThreats リストは、その無害化が魔法ではなく監査可能であることを意味します。

「CDR は墨消しにステップを足しただけだ。」

そうではなく、両者を混同するのは危険です。墨消しは 情報 を除去します — 名前、口座番号、人が見てはならないコンテンツです。CDR は 能力 を除去します — JavaScript、Launch アクション、機械が実行してはならない埋め込みペイロードです。両者は正反対の成功基準を持ちます。墨消しは、機微な コンテンツ が消え、残りがそっくり保存されているとき正しい。無害化は、脅威 が消えたとき正しく、そこへ至るために良性の構造を変えることをいとわない。あなたの意図に合うツールを使ってください。一方を、他方の保証を期待して手に取らないでください。

第 2 の誤解は、クリーンな再構築が元が clean だったことを証明するというものです。それは元について何も証明しません。それは 出力 が検出された脅威を含まないことだけを証明します。入力は武器だったかもしれません。CDR の仕事は、あなたが下流へ転送するものがそうでないと確かにすることです。

これはパンフレットが飛ばす部分なので、率直に言います。

  • 出力はセキュリティ投影であって、証拠としてのコピーではありません。 CdrEngine のソースは、これをアーキテクチャ上の越えてはならない一線として携えます。サニタイズされた PDF は、法的な証拠保全のため、元との比較のためのハッシュ、あるいはアーカイブコピーとして使われては なりません。変換は設計上、破壊的で非可逆です。
  • 検出には範囲があります。 CDR は、名付け方を知っている脅威を除去します。それは多層防御スタックにおける強力で監査可能なレイヤーであって — ファイルがあらゆる将来の手法から自由であることの保証ではありません。OWASP のファイルアップロード指針が記述するのと同じアップロード検証、コンテンツタイプチェック、そして最小権限の取り扱いの背後に、それを保ってください。
  • いくつかのポリシーは意図的に損失を伴います。 オプトインの Strip* のケースは、埋め込みファイル、署名、フォームフィールド、レイヤー、そして 3D メディアを除去します。それらは強力で、正規のコンテンツ — たとえば ZUGFeRD/Factur-X のインボイスペイロード — を削除します。まさにその理由で、それらはデフォルトでオフです。承知のうえでオンにしてください。
  • 署名は再構築を生き延びません。 ファイルを再構築するとそのバイトが変わるので、元の電子署名はもはやそのバイト範囲と一致しません。無害化された文書は、ソースに対して署名されていません。署名済みのアーティファクトが必要なら、新たな行為として クリーンな 出力に署名してください。
Content Disarm & Reconstruction (CDR) — edition availability
EditionAvailability
Core

Not available. NextPDF core does not perform CDR. It parses, renders, and writes PDFs; it does not threat-detect or rebuild untrusted input.

Pro

Not available in the Pro edition.

Enterprise

Available via CdrEngine. The disarmed output is a security projection of the source — it must not be treated as an evidentiary, hash-comparable, or archival copy of the original.

  • How PDF encryption really works — 機微な PDF を扱うもう半分。コンテンツを守ることと能力を除去すること。
  • Errors as a feature — CDR の受け入れ制御による拒否と /Root ガードが体現する、フェイルクローズの哲学。
  • An API that refuses to guess — なぜ クリーンにした拒否した を区別する結果型が、無言のベストエフォートに勝るのか。
  • CDR(Content Disarm & Reconstruction) — 信頼できないファイルをパースし、能動的または危険なコンポーネントを除去し、安全な残りからクリーンなファイルを再構築するサニタイズ戦略で、既知の悪のシグネチャをスキャンするものではない。
  • セキュリティ投影(Security projection) — 脅威の除去を保証しつつ、有用であるに 十分な だけソースを保存するサニタイズ済み出力。意図的にバイト忠実ではなく、証拠、ハッシュ、あるいはアーカイブには適さない。
  • 受け入れ制御(Admission control) — 無害化作業が始まる前に、リソース制限(オブジェクト数、ページ数、デコード済みストリームサイズ、インフレーション比率)を超える文書を拒否する、サニタイズ前のゲート。
  • アクション(Action) — 文書を開く、フィールドが変わるなどのトリガーで何かを起こす PDF の構造。アクション型のカタログ(Spec: ISO 32000-2, §12.6.4)は JavaScript と Launch アクション、すなわち正準の CDR 脅威面を含む。
  • ダングリング参照(Dangling reference) — フィルタの後にもはや存在しないオブジェクトへのポインタ。参照スクラブのフェーズがこれらを除去するので、再構築されたファイルは構造的に整合したまま保たれる。