Technical Article

DelphiにおけるPDFiumを使用したPDFセキュリティリスクの監査

PDFは単なる紙ではありません。ファイルが開いたときに実行されるスクリプト、外部プログラムを起動するリンク、Webサーバーにアクセスするリンク、ファイル内にネストされたファイル、そして誰かが保証した後にドキュメントが変更されていないことを証明する署名を保持できるコンテナです。制御できないソースからファイルが届いた場合、最も安全な最初のステップはレンダリングすることではありません。ファイル自身に関する記述を読み取り、実行しようとする可能性のあるすべての項目のインベントリ(一覧)を作成することです。それにより、人間がそれをワークフローに入れるべきかどうかを判断できます。

この記事では、DelphiおよびLazarus向けのPDFiumコンポーネントを使用して、そのリスク領域に対して静的な読み取り専用の監査を実行する手順を説明します。監査ではページを決して描画しません。ドキュメント構造をパースし、動作を伴うファイルの部分を列挙し、プレーンなレポートを書き出します。これは、見知らぬ人に玄関でポケットの中身を空にするよう求めることと、彼らが微笑んだからといって信頼することの違いです。

監査とは何か、そして何ではないか

境界線を明確にしましょう。サンドボックス化されたプレビューは、厳しい制限の下でファイルをレンダリングし、ファイルがマシンの他の部分に触れることなくユーザーが内容を確認できるようにします。監査はその前に実行されます。これは描画を伴わない検査であり、その出力は脅威領域の説明(どのスクリプトが存在するか、どのリンクにどのアクションが定義されているか、ファイルが署名されているかとその強度、および何が添付されているか)のみです。ドキュメントが信頼境界を越えるとき、つまり電子メールからの受信、アップロードフォーム、またはパートナーからのフィード時など、後続のステージで実際に開く前に実行します。

コンポーネントは、他の処理と同様に監査でもドキュメントを同じ方法で読み込みます。ファイル名を設定してアクティブにすると、1ページもレンダリングすることなく、相互参照データとドキュメントカタログがパースされます。以下はすべて、その読み込まれた描画前の状態からデータを読み取ります。

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Incoming_Invoice.pdf';
    Pdf.Active := True;          // parses structure, renders nothing
    // audit the loaded document here
  finally
    Pdf.Free;
  end;
end;

名前ツリー内のドキュメントJavaScript

最初に列挙すべきなのはコードです。PDFはドキュメントレベルのJavaScript(特定のページやフィールドに関連付けられておらず、ドキュメント自体に関連付けられ、/Namesツリーの/JavaScriptエントリの下に保存されているスクリプト)を保持できます。規格に準拠したビューアは、これを開いたときに実行します。これは、PDFマルウェアの長い歴史の背景にあるメカニズムです。なぜなら、ユーザーが文字を読む前に、ダブルクリックした瞬間にファイルにロジックを実行させることができるからです。

印刷やフォーム送信時に動作するJavaScriptスクリプトも存在する可能性があるためです。しかし、ドキュメントスクリプトを持つファイルは常に再確認する価値があります。存在数だけでも有用なチェック項目ですが、スクリプト本体は、チェックを最終的な判断へとつなげる要素になります。

var
  I: Integer;
  Action: TPdfJavaScriptAction;
begin
  if Pdf.JavaScriptActionCount > 0 then
    WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
            ' script(s) on open');
  for I := 0 to Pdf.JavaScriptActionCount - 1 do
  begin
    Action := Pdf.JavaScriptAction[I];
    WriteLn('  script "', Action.Name, '":');
    WriteLn(Action.Script);   // full body, for a human to read
  end;
end;

ドキュメントスクリプトがゼロのファイルが自動的に安全というわけではありません。ページやフィールドのスクリプトも存在する可能性があるためです。しかし、ドキュメントスクリプトを持つファイルは常に再確認する価値があります。存在数だけでも有用なチェック項目ですが、スクリプト本体は、チェックを最終的な判断へとつなげる要素になります。

起動(Launch)およびURIアクション

次にインベントリを作成すべき動作は、リンクと注釈にあります。監査人にとって最も重要なアクションタイプは2つあります。起動(Launch)アクションは、リンクがトリガーされたときに外部プログラムを開始するか、ローカルファイルを開きます。URIアクションは、Webターゲットを開きます。不審なドキュメントを確認するレビュー担当者は、何もクリックすることなく、3ページのボタンがcmd.exeを起動するように定義されていることや、ページ上のブランドと一致しないURLを開くように定義されていることを確認できる必要があります。

コンポーネントは見つかったリンクを分類し、それぞれのアクションタイプとターゲットパスを公開するため、監査処理はすべての起動およびURIアクションをその宛先とともにリスト化できます。これはレポートの作成であり、実行ではありません。監査人は構造からアクションを読み取って記録します。それを追跡して実行することはありません。

ドキュメントをレンダリングするビューアコントロールは、アクションの実行が発生する場所であり、そのデフォルト設定は意図的に慎重なものになっています。TPdfViewコントロールは、クリック時にどのアクションタイプを自動的に起動するかを決定するLinkOptionsセットを保持します。デフォルトは[loAutoGoto, loAutoOpenURI]であり、ドキュメント内のジャンプやWeb URLは開く可能性がありますが、loAutoLaunchは含まれていないため、起動アクションが自動的に実行されることはありません。監査ワークフローにおいては、さらに進んでこのセットを完全にクリアすることで、ファイルが信頼できるかどうかを決定している間、何も自動起動しないようにします。

// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];

// The shipped default already withholds launch:
//   default = [loAutoGoto, loAutoOpenURI]
//   loAutoLaunch is NOT in the default set, so external programs
//   are never started on a stray click out of the box.

デフォルトで起動を制限する理由はシンプルです。ドキュメント内のジャンプは無害であり、URLは視認できキャンセル可能です。しかし、クリックから任意の外部プログラムを開始することは、PDFリンクが要求しうる中で最も危険な動作であるため、明示的にオプトインしない限りオフになります。監査人は、安全な動作であってもオプトアウトします。仕事は見ることであり、実行することではないからです。

デジタル署名のMDP許可レベル

署名は状況を変えます。プレーンな署名は、署名時のバイトデータを証明します。ドキュメントの変更検出および防止(MDP)ルールで作成された認証署名は、さらに進んで、ドキュメントが認証された後に合法的に変更できる内容を宣言し、準拠したビューアは、その許容範囲外のものが変更された場合に警告を出します。その許可レベルを読み取ることで、ファイルが認証されているかどうか、また、認証されている場合にどの程度ロックダウンされるべきであるかを監査人に伝えます。

MDP許可は、3つの値が定義された整数です。レベル1は、一切の変更が許可されていないことを意味します。いかなる変更も認証を無効にします。レベル2は、フォームへの入力と署名を許可します。これは、入力して署名するがそれ以外の変更は行わない契約書で一般的なケースです。レベル3は、フォーム入力と署名に加えて、さらに注釈を許可します。このレベルを知ることで、受信ロジックは意図を判断できます。レベル1で認証されているにもかかわらず、フォームフィールドやスクリプトを保持しているドキュメントは矛盾しており、その矛盾はフラグを立てる価値があります。

コンポーネントは署名の数を読み取り、それぞれをPermissionフィールドにそのMDP値を保持するレコードとして公開します。これは下層のFPDFSignatureObj_GetDocMDPPermission呼び出しから直接取得されます。許可値が0の場合、その署名は認証(DocMDP)署名ではないため、報告すべきドキュメントレベルのロックダウンはありません。

var
  I: Integer;
  Sig: TPdfSignature;
begin
  if Pdf.SignatureCount = 0 then
    WriteLn('document is not signed')
  else
    for I := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[I];
      case Sig.Permission of
        1: WriteLn('certified: no changes allowed');
        2: WriteLn('certified: form fill and signing allowed');
        3: WriteLn('certified: form fill, signing and annotations allowed');
      else
        WriteLn('signed, but not a DocMDP certification');
      end;
    end;
end;

監査はここで署名の暗号的な検証は行いません。証明書チェーンの検証は別の問題です。レポートするのは、宣言された意図(このファイルはこのレベルでロックされているという記述)です。これはまさに、後からの変更やアクティブコンテンツの存在が、作成者がドキュメントをシールした方法と一致しているかどうかをレビュー担当者が判断するために必要なコンテキストです。

その他の領域:埋め込みファイルとXFA

さらに2つの項目で完全なインベントリが完成します。埋め込みファイルは、添付ファイルとしてPDFの内部に保持されるドキュメント全体であり、典型的な攻撃の媒介となります。無害に見えるレポートの添付ファイルツリー内に、実行可能ファイルや2つ目の悪意のあるPDFが含まれている可能性があるためです。コンポーネントは添付ファイルの数とそれぞれの名前を公開するため、監査処理は、それらを展開したり開いたりすることなく、添付されている内容をリスト化できます。

XFAの存在はもう1つのフラグです。XFAフォームは、静的なAcroFormを、独自のレンダリングとスクリプトモデルを備えたXMLベースのフォームアーキテクチャに置き換えます。これはプレーンなフォームよりも大きく複雑な領域です。XFAが存在することを記録するためにXFAを処理する必要はありません。その存在自体が、ファイルがよりリッチなインタラクティブレイヤーを保持しており、再確認する価値があることを示すシグナルです。コンポーネントはこれを単一のブール値として報告します。

var
  I: Integer;
begin
  if Pdf.XFA then
    WriteLn('NOTE: document contains an XFA form layer');

  if Pdf.AttachmentCount > 0 then
  begin
    WriteLn('embedded files: ', Pdf.AttachmentCount);
    for I := 0 to Pdf.AttachmentCount - 1 do
      WriteLn('  - ', Pdf.AttachmentName[I]);
  end;
end;

レポートを書き出す1つの読み取り専用ルーチン

これらを組み合わせると、監査は、ドキュメントを読み込み、スクリプトとそのテキストを列挙し、起動およびURIターゲットをリスト化し、署名のMDPレベルを報告し、添付ファイルとXFAを記録して、その結果をログに書き出す単一のプロシージャになります。描画を行わないため、処理負荷が低く、悪意のあるページコンテンツを表示させられる心配もありません。出力は、レビュー担当者や後続のルールが実行できる、フラットで人間が読めるレポートです。

実用的な構成は、各結果を行として収集し、危険度の高いものにプレフィックスを付けてレビューキューの最上部にソートされるようにし、ファイルに隣接して保存することです。スクリプト、起動アクション、添付ファイル、XFAがなく、署名がないか一貫した認証のみを持つドキュメントは、警告なしでクリアされます。一度に複数のフラグをトリップするドキュメントは、後続のステージで開かれる前に人間が確認すべきものです。監査は信頼の決定自体を行うわけではありません。決定がブラインドではなく、情報に基づいて行われるようにするものです。

ファイルが監査をクリアし、内容を確認する必要がある場合は、デフォルトのビューアではなく制限された環境で確認してください。Delphiにおける安全なPDFプレビューの構築に関するチュートリアルのアプローチは、制御された閲覧中にリンクの自動処理やアクティブコンテンツの動作を防ぐ方法を示しています。この列挙手順を、レビューツールを備えた完全な受信パイプラインに組み込むには、PDF受信およびレビューワークベンチに関する記事を参照してください。どちらも、同じ読み取り専用でレンダリングを行わない基盤の上に構築されており、このブログの他の場所で説明されている描画、テキスト、フォーム、署名APIと並んで、DelphiおよびC++Builder向けのPDFium Componentの一部として提供されています。