Technical Article

Mengaudit Risiko Keamanan PDF dengan PDFium di Delphi

PDF bukan sekadar kertas. Ia adalah wadah yang dapat membawa skrip yang berjalan saat berkas dibuka, tautan yang memulai program eksternal, tautan yang menjangkau server web, berkas yang bersarang di dalam berkas, dan tanda tangan yang menyatakan bahwa dokumen tersebut tidak berubah sejak seseorang menjaminnya. Ketika berkas tiba dari sumber yang tidak Anda kendalikan, langkah pertama yang paling aman bukanlah merendernya. Ini adalah membaca apa yang dikatakan berkas tentang dirinya sendiri dan membuat inventaris tentang semua yang dapat dicobanya, sehingga manusia dapat memutuskan apakah berkas tersebut termasuk dalam alur kerja Anda atau tidak sama sekali.

Artikel ini membahas langkah audit statis, baca-saja di atas permukaan risiko tersebut menggunakan komponen PDFium untuk Delphi dan Lazarus. Audit tidak pernah melukis halaman. Ia mengurai struktur dokumen, menghitung bagian-bagian berkas yang membawa perilaku, dan menulis laporan biasa. Ini adalah perbedaan antara meminta orang asing mengosongkan saku mereka di pintu dan memercayai mereka karena mereka tersenyum.

Apa itu audit, dan apa yang bukan

Perjelas tentang batasannya. Pratinjau kotak pasir (sandboxed preview) merender berkas di bawah pembatasan ketat sehingga pengguna dapat melihatnya tanpa berkas tersebut menyentuh bagian mesin lainnya. Audit datang sebelum itu. Ini adalah pemeriksaan bebas render yang keluaran satu-satunya adalah deskripsi permukaan ancaman (threat surface): skrip mana yang ada, tindakan mana yang dihubungkan ke tautan, apakah berkas ditandatangani dan seberapa ketat, dan apa yang dilampirkan. Anda menjalankannya ketika dokumen melintasi batas kepercayaan, pada penerimaan dari email, formulir unggahan, atau umpan mitra, sebelum tahap selanjutnya membukanya secara nyata.

Komponen memuat dokumen dengan cara yang sama untuk audit seperti untuk hal lainnya. Anda menyetel nama berkas dan mengaktifkannya, yang mengurai data referensi silang dan katalog dokumen tanpa merender satu halaman pun. Semua yang ada di bawah dibaca dari keadaan yang dimuat dan tidak dirender tersebut.

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;

Dokumen JavaScript di pohon nama (name tree)

Hal pertama yang harus dihitung adalah kode. PDF dapat membawa JavaScript tingkat dokumen: skrip yang tidak dilampirkan ke halaman atau field apa pun tetapi ke dokumen itu sendiri, disimpan di pohon /Names di bawah entri /JavaScript. Pembaca dokumen yang patuh menjalankan ini saat dibuka. Itulah mekanisme di balik barisan panjang malware PDF, karena memungkinkan berkas mengeksekusi logika saat pengguna mengklik dua kali, sebelum mereka membaca sepatah kata pun.

Seorang auditor menginginkan dua fakta tentang setiap skrip tersebut: bahwa skrip itu ada, dan apa isinya. Komponen mengekspos jumlahnya dan memungkinkan Anda membaca setiap tindakan sebagai rekaman yang menyimpan nama skrip dan badan lengkapnya. Membaca badan skrip penting. Skrip bernama Doc.0 tidak memberi tahu Anda apa-apa, tetapi teksnya mungkin memanggil app.launchURL atau menyusun string dan meneruskannya ke tempat yang tidak seharusnya. Menarik sumber keluar agar peninjau dapat membacanya adalah seluruh poin dari menandai berkas yang menjalankan kode saat dibuka.

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;

Berkas dengan nol skrip dokumen tidak secara otomatis aman, karena skrip halaman dan field juga ada, tetapi berkas dengan skrip dokumen selalu layak mendapatkan pandangan kedua. Jumlah kehadiran saja adalah gerbang yang berguna, dan badan skrip adalah apa yang mengubah gerbang menjadi penilaian.

Tindakan Launch dan URI

Perilaku berikutnya untuk diinventarisasi hidup di tautan dan anotasi. Dua jenis tindakan paling penting bagi auditor. Tindakan Launch memulai program eksternal atau membuka berkas lokal saat tautan dipicu. Tindakan URI membuka target web. Peninjau yang melihat dokumen mencurigakan harus dapat melihat, tanpa mengklik apa pun, bahwa tombol di halaman tiga dihubungkan untuk meluncurkan cmd.exe atau membuka URL yang tidak cocok dengan merek di halaman tersebut.

Komponen mengklasifikasikan tautan yang ditemukannya dan mengekspos tipe tindakan serta jalur target untuk masing-masing, sehingga audit dapat mencantumkan setiap tindakan Launch dan URI beserta tujuannya. Ini adalah pelaporan, bukan eksekusi. Auditor membaca tindakan dari struktur dan menuliskannya. Ia tidak pernah mengikutinya.

Kontrol pembaca (viewer control) yang merender dokumen adalah tempat di mana tindakan berikut akan terjadi, dan postur default-nya sengaja dibuat hati-hati. Kontrol TPdfView memiliki kumpulan LinkOptions yang memutuskan jenis tautan mana yang aktif secara otomatis pada sebuah klik. Default-nya adalah [loAutoGoto, loAutoOpenURI], yang berarti lompatan di dalam dokumen dan URL web dapat dibuka, tetapi loAutoLaunch tidak ada, sehingga tindakan peluncuran tidak pernah berjalan secara otomatis. Untuk alur kerja audit, Anda melangkah lebih jauh dan membersihkan kumpulan tersebut sepenuhnya, sehingga tidak ada yang aktif secara otomatis saat Anda masih memutuskan apakah akan memercayai berkas tersebut.

// 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.

Alasan di balik penahanan peluncuran (launch) secara default sederhana. Lompatan di dalam dokumen tidak berbahaya dan URL terlihat serta dapat dibatalkan, tetapi memulai program eksternal arbitrer dari sebuah klik adalah hal paling berbahaya yang dapat diminta oleh tautan PDF, sehingga dinonaktifkan kecuali Anda memilih untuk mengaktifkannya. Auditor memilih keluar bahkan dari perilaku yang aman, karena tugasnya adalah melihat, bukan bertindak.

Tingkat izin MDP tanda tangan digital

Tanda tangan mengubah pertanyaan. Tanda tangan biasa membuktikan byte pada saat penandatanganan. Tanda tangan sertifikasi, jenis yang dibuat dengan aturan deteksi dan pencegahan modifikasi dokumen (document modification detection and prevention), melangkah lebih jauh: ia menyatakan apa saja yang secara sah dapat berubah setelah dokumen disertifikasi, dan pembaca dokumen yang patuh memperingatkan jika ada sesuatu di luar izin tersebut yang disentuh. Membaca tingkat izin tersebut memberi tahu auditor apakah berkas tersebut disertifikasi dan, jika ya, seberapa terkunci dokumen tersebut seharusnya.

Izin MDP adalah integer dengan tiga nilai yang ditentukan. Tingkat 1 berarti tidak ada perubahan yang diizinkan sama sekali; modifikasi apa pun membatalkan sertifikasi. Tingkat 2 mengizinkan pengisian formulir dan penandatanganan, kasus umum untuk kontrak yang dimaksudkan untuk diselesaikan dan ditandatangani tetapi tidak untuk diubah. Tingkat 3 selain itu mengizinkan anotasi di atas pengisian formulir dan penandatanganan. Mengetahui tingkat tersebut memungkinkan logika penerimaan Anda menalar tentang niat: dokumen yang disertifikasi pada tingkat 1 yang tetap membawa field formulir atau skrip bertentangan dengan dirinya sendiri, dan kontradiksi tersebut layak ditandai.

Komponen membaca jumlah tanda tangan dan mengekspos masing-masing sebagai rekaman yang bidang Permission miliknya membawa nilai MDP tersebut, diisi langsung dari panggilan FPDFSignatureObj_GetDocMDPPermission yang mendasarinya. Izin nol berarti tanda tangan tersebut bukan tanda tangan sertifikasi (DocMDP), sehingga tidak ada kuncian tingkat dokumen yang perlu dilaporkan.

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;

Audit tidak memvalidasi kriptografi tanda tangan di sini; memverifikasi rantai sertifikat adalah masalah terpisah. Apa yang dilaporkannya adalah niat yang dinyatakan: berkas ini mengatakan ia dikunci pada tingkat ini. Itulah konteks yang dibutuhkan peninjau untuk menilai apakah perubahan di kemudian hari, atau sekadar kehadiran konten aktif, konsisten dengan cara penulis menyegel dokumen.

Sisa permukaan: berkas tertanam dan XFA

Dua item lagi melengkapi inventaris lengkap. Berkas tertanam (embedded files) adalah seluruh dokumen yang dibawa di dalam PDF sebagai lampiran, dan merupakan kendaraan pengiriman klasik, karena laporan yang tampak jinak dapat mengirimkan berkas eksekusi (executable) atau PDF berbahaya kedua di pohon lampirannya. Komponen mengekspos jumlah lampiran dan nama masing-masing lampiran, sehingga audit dapat mencantumkan apa yang ikut serta tanpa mengekstrak atau membuka apa pun dari berkas tersebut.

Kehadiran XFA adalah bendera lainnya. Formulir XFA menggantikan AcroForm statis dengan arsitektur formulir berbasis XML yang membawa model perenderan dan skripnya sendiri, permukaan yang lebih besar dan lebih kompleks daripada formulir biasa. Anda tidak perlu memproses XFA untuk mencatat kehadirannya; kehadirannya saja adalah sinyal bahwa berkas membawa lapisan interaktif yang lebih kaya yang layak dilihat lebih dekat. Komponen melaporkannya sebagai boolean tunggal.

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;

Satu rutinitas baca-saja yang menulis laporan

Satukan potongan-potongan tersebut dan audit adalah prosedur tunggal yang memuat dokumen, menghitung skrip dan badannya, mencantumkan target Launch dan URI, melaporkan tingkat MDP tanda tangan, mencatat lampiran serta XFA, dan menulis temuan ke log. Ia tidak merender apa pun, sehingga murah dan tidak dapat dikelabui untuk menampilkan konten halaman yang bermusuhan. Keluarannya adalah rekaman datar yang dapat dibaca manusia yang dapat ditindaklanjuti oleh peninjau atau aturan hilir.

Bentuk yang berfungsi dengan baik dalam praktik adalah mengumpulkan setiap temuan sebagai baris, memberi awalan pada temuan yang benar-benar berisiko sehingga mereka berada di urutan teratas antrean peninjauan, dan menyimpan semuanya di samping berkas. Dokumen tanpa skrip, tanpa tindakan peluncuran, tanpa lampiran, tanpa XFA, dan tanpa tanda tangan atau sertifikasi yang koheren akan lolos dengan tenang. Dokumen yang memicu beberapa bendera sekaligus adalah dokumen yang harus dilihat seseorang sebelum tahap selanjutnya membukanya. Audit tidak membuat keputusan kepercayaan untuk Anda. Ia memastikan keputusan tersebut didasarkan pada informasi, bukan buta.

Setelah berkas lolos audit dan Anda memang perlu melihatnya, lakukan di bawah batasan alih-alih di pembaca dokumen default. Pendekatan dalam panduan kami tentang membangun pratinjau PDF aman di Delphi menunjukkan cara menjaga penanganan otomatis tautan dan konten aktif agar tidak bertindak selama tampilan terkontrol. Untuk melipatgandakan penghitungan ini ke dalam pipeline penerimaan penuh dengan perkakas peninjau, lihat artikel meja kerja penerimaan dan peninjauan PDF. Keduanya dibangun di atas fondasi baca-saja, bebas render yang sama dan dikirim sebagai bagian dari PDFium Component for Delphi and C++Builder, bersama dengan API perenderan, teks, formulir, dan tanda tangan yang dibahas di bagian lain di blog ini.