Technical Article

メモリセーフなPDF解析: 悪意のあるドキュメントからの防御

PDFドキュメントは信じられないほど強力ですが、その強力さには固有のセキュリティリスクが伴います。PDFは埋め込みファイル、インタラクティブなJavaScript、複雑なバイナリストリームをサポートしているため、マルウェアの配信ベクターとして頻繁に使用されます。不適切に記述されたPDFパーサーのバッファオーバーフロー、境界外読み取り、および整数オーバーフローは、リモートコード実行(RCE)につながる可能性があります。

DelphiでユーザーがアップロードしたPDFを受け入れるアプリケーション(たとえば、ドキュメント取り込みポータル)を構築している場合、メモリセーフなPDF解析を確保することは不可欠なセキュリティ要件です。

一般的なPDFの攻撃ベクター

悪意のあるPDFは通常、オペレーティングシステムではなくパーサー自体の脆弱性を標的とします。一般的な手法は次のとおりです。

  • 不正な相互参照(XRef)テーブル: 境界外につながるポインタオフセットを作成し、パーサーをクラッシュさせたり、メモリの開示を許可したりします。
  • 無限ループ: PDFオブジェクト間で循環参照を作成し(たとえば、オブジェクトAがオブジェクトBを参照し、それがオブジェクトAを参照する)、スタックの枯渇を引き起こします。
  • 爆発的な解凍(Zip爆弾): 数キロバイトから数ギガバイトに解凍されるFlateDecodeストリームによって、システムメモリを枯渇させます。

Delphiにおける防御的解析戦略

DelphiでPDFをネイティブに解析する場合は、防御的なプログラミングを行う必要があります。PDF辞書で提供されるメタデータを信頼することはできません。

1. 循環参照の切断

PDFオブジェクトツリーを再帰的に探索する場合、無限ループを防ぐために、訪問したオブジェクトの履歴を保持する必要があります。

uses
  System.Generics.Collections, System.SysUtils;

// A safe recursive function to walk the PDF tree
procedure ParsePDFDictionary(DictObj: TPDFDictionary; Visited: TList<Integer>);
var
  ObjID: Integer;
begin
  ObjID := DictObj.ObjectID;
  
  if Visited.Contains(ObjID) then
  begin
    Writeln('Warning: Circular reference detected. Aborting branch.');
    Exit;
  end;
  
  Visited.Add(ObjID);
  
  try
    // Process child objects safely...
  finally
    // Allow siblings to traverse, but prevent vertical recursion loops
    Visited.Remove(ObjID);
  end;
end;

2. Zip爆弾からの保護

ストリームを解凍するためにFlateDecodeフィルターを適用する場合は、最大展開サイズを厳密に制限する必要があります。`/Length`辞書キーに基づいて盲目的にメモリを割り当ててはいけません。

const
  MAX_DECOMPRESSED_SIZE = 1024 * 1024 * 50; // 50 MB safety limit

procedure DecompressPDFStream(CompressedStream, OutputTarget: TStream);
var
  ZLibStream: TZDecompressionStream;
  Buffer: array[0..8191] of Byte;
  BytesRead, TotalRead: Integer;
begin
  ZLibStream := TZDecompressionStream.Create(CompressedStream);
  try
    TotalRead := 0;
    repeat
      BytesRead := ZLibStream.Read(Buffer[0], SizeOf(Buffer));
      if BytesRead > 0 then
      begin
        TotalRead := TotalRead + BytesRead;
        if TotalRead > MAX_DECOMPRESSED_SIZE then
          raise Exception.Create('Security Exception: Decompression bomb detected!');
          
        OutputTarget.WriteBuffer(Buffer[0], BytesRead);
      end;
    until BytesRead = 0;
  finally
    ZLibStream.Free;
  end;
end;

堅牢化されたエンジンと安全なコンポーネントの活用

完全に安全な PDF パーサーをゼロから記述することは非常に困難な作業です。業界標準のアプローチは、PDFium のような堅牢化され、徹底的にファジングテストされたエンジンを使用するか、厳密にテストされたネイティブライブラリに依存することです。

PDFium は Google Chrome で使用されているコアレンダリングエンジンです。Chrome は毎日何百万もの信頼できない PDF を処理するため、PDFium は Google の Project Zero による積極的かつ継続的なファジングを受けています。不正な形式の XRef、破損したストリーム、および循環参照を適切に処理します。

同様に、HotPDF ComponentDelphi PDF Library などのネイティブコンポーネントは、強力な防御的解析戦略を標準で組み込んでいます。これらは、Delphi および C++Builder 環境向けに特別に設計された、厳密な境界チェック、再帰的な深度制限、およびメモリリーク防止メカニズムを実装しています。

レンダリングのために Delphi ラッパーを介して PDFium を利用する場合でも、ドキュメントの生成や処理に HotPDF のようなネイティブコンポーネントを使用する場合でも、自身で防御的なパーサーを記述することなく、エンタープライズグレードのセキュリティ境界を継承し、ユーザーやサーバーを悪意のあるペイロードから保護できます。

注: 安全でファジングテスト済みの解析機能は、HotPDF ComponentDelphi PDF Library、および PDFium Component を含む当社のスイート全体で利用可能です。