Technical Article

Kiểm tra rủi ro bảo mật PDF với PDFium trong Delphi

Một tài liệu PDF không chỉ đơn thuần là giấy. Nó là một thùng chứa có thể mang các kịch bản (script) chạy khi tệp mở ra, các liên kết khởi động các chương trình bên ngoài, các liên kết tiếp cận máy chủ web, các tệp lồng bên trong các tệp, và một chữ ký khẳng định tài liệu không thay đổi kể từ khi có ai đó xác nhận nó. Khi một tệp đến từ một nguồn bạn không kiểm soát, bước đi đầu tiên an toàn nhất không phải là kết xuất nó. Đó là đọc những gì tệp nói về chính nó và xây dựng một danh mục mọi thứ nó có thể cố gắng thực hiện, để con người có thể quyết định xem nó có thuộc về quy trình làm việc của bạn hay không.

Bài viết này đi qua một lượt kiểm tra (audit) tĩnh, chỉ đọc trên bề mặt rủi ro đó bằng cách sử dụng thành phần PDFium dành cho Delphi và Lazarus. Cuộc kiểm tra không bao giờ vẽ một trang. Nó phân tích cú pháp cấu trúc tài liệu, liệt kê các phần của tệp mang hành vi, và viết một báo cáo phẳng. Đó là sự khác biệt giữa việc yêu cầu một người lạ móc hết túi của họ tại cửa ra vào và tin tưởng họ chỉ vì họ đã mỉm cười.

Kiểm tra là gì, và nó không phải là gì

Hãy rõ ràng về ranh giới. Bản xem trước hộp cát (sandboxed preview) kết xuất một tệp dưới các hạn chế chặt chẽ để người dùng có thể xem nó mà không làm tệp chạm vào phần còn lại của máy tính. Một cuộc kiểm tra đến trước điều đó. Nó là một cuộc kiểm tra không cần kết xuất (render-free inspection) với đầu ra duy nhất là mô tả về bề mặt đe dọa (threat surface): script nào tồn tại, action nào được kết nối với các liên kết, tệp có được ký hay không và chặt chẽ thế nào, và những gì được đính kèm. Bạn chạy nó khi một tài liệu vượt qua ranh giới tin cậy, khi tiếp nhận từ email, biểu mẫu tải lên, hoặc nguồn dữ liệu đối tác, trước khi bất kỳ giai đoạn sau nào thực sự mở nó.

Thành phần tải một tài liệu cho cuộc kiểm tra theo cùng một cách như bất kỳ việc nào khác. Bạn đặt tên tệp và kích hoạt nó, thao tác này phân tích cú pháp dữ liệu tham chiếu chéo (cross-reference) và catalog tài liệu mà không kết xuất một trang nào. Mọi thứ bên dưới đọc từ trạng thái đã tải, không kết xuất đó.

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;

Document JavaScript trong cây tên (name tree)

Điều đầu tiên cần liệt kê là mã nguồn. Một tài liệu PDF có thể mang JavaScript cấp tài liệu (document-level JavaScript): các script không được gắn vào bất kỳ trang hoặc trường nào mà gắn vào chính tài liệu, được lưu trữ trong cây /Names dưới một mục nhập /JavaScript. Một trình xem tuân thủ tiêu chuẩn sẽ chạy các mã này khi mở tệp. Đó là cơ chế đằng sau một hàng dài mã độc PDF, bởi vì nó cho phép một tệp thực thi logic ngay lập tức khi người dùng nhấp đúp vào nó, trước khi họ kịp đọc một từ.

Một người kiểm tra muốn biết hai thực tế về mỗi script như vậy: rằng nó tồn tại, và nó chứa những gì. Thành phần hiển thị số lượng và cho phép bạn đọc mỗi action như một bản ghi giữ tên của script và toàn bộ phần thân của nó. Đọc phần thân rất quan trọng. Một script tên Doc.0 không cho bạn biết điều gì, nhưng văn bản của nó có thể gọi app.launchURL hoặc lắp ráp một chuỗi và truyền nó đến một nơi không nên đến. Trích xuất mã nguồn ra để người phản biện có thể đọc là toàn bộ mục đích của việc gắn cờ một tệp chạy mã khi mở.

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;

Một tệp có script tài liệu bằng không không tự động an toàn, bởi vì các script của trang và trường cũng tồn tại, nhưng một tệp có script tài liệu luôn đáng được xem xét lại lần hai. Chỉ riêng số lượng hiện diện đã là một cổng hữu ích, và phần thân là thứ biến một chiếc cổng thành một phán quyết.

Action Launch và URI

Hành vi tiếp theo cần kiểm kê nằm trên các liên kết và chú thích. Hai loại action quan trọng nhất đối với một người kiểm tra. Một action Launch khởi động một chương trình bên ngoài hoặc mở một tệp cục bộ khi liên kết được kích hoạt. Một action URI mở một mục tiêu web. Một người phản biện đang xem xét một tài liệu đáng ngờ phải có khả năng nhìn thấy, mà không cần nhấp vào bất cứ thứ gì, rằng một nút trên trang ba được kết nối để khởi chạy cmd.exe hoặc để mở một URL không khớp với thương hiệu trên trang.

Thành phần phân loại các liên kết mà nó tìm thấy và hiển thị loại action và đường dẫn đích cho mỗi liên kết, do đó một cuộc kiểm tra có thể liệt kê mọi action Launch và URI với đích đến của nó. Đây là báo cáo, không phải thực thi. Người kiểm tra đọc action ra khỏi cấu trúc và ghi lại. Nó không bao giờ chạy theo liên kết đó.

Điều khiển trình xem kết xuất tài liệu là nơi việc đi theo một action sẽ xảy ra, và tư thế mặc định của nó là cố ý thận trọng. Điều khiển TPdfView có một tập hợp LinkOptions quyết định loại liên kết nào tự động kích hoạt khi nhấp chuột. Mặc định của nó là [loAutoGoto, loAutoOpenURI], có nghĩa là các cú nhảy trong tài liệu và URL web có thể mở, nhưng loAutoLaunch vắng mặt, vì vậy các action launch không bao giờ tự động chạy. Đối với một quy trình kiểm tra, bạn tiến xa hơn và xóa sạch hoàn toàn tập hợp này, do đó không có gì tự động kích hoạt trong khi bạn vẫn đang quyết định xem có nên tin tưởng tệp hay không.

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

Lập luận đằng sau việc từ chối launch theo mặc định rất đơn giản. Một cú nhảy bên trong tài liệu là vô hại và một URL hiển thị rõ ràng và có thể hủy bỏ, nhưng khởi động một chương trình bên ngoài tùy ý từ một cú nhấp chuột là điều nguy hiểm nhất mà một liên kết PDF có thể yêu cầu, vì vậy nó bị tắt trừ khi bạn chọn tham gia. Một người kiểm tra chọn từ chối ngay cả các hành vi an toàn, bởi vì công việc là để nhìn, chứ không phải để hành động.

Mức độ quyền MDP của chữ ký số

Chữ ký thay đổi câu hỏi. Một chữ ký đơn giản chứng thực các byte tại thời điểm ký. Một chữ ký chứng nhận (certification signature), loại được tạo ra với một quy tắc phát hiện và ngăn chặn sửa đổi tài liệu (document modification detection and prevention), tiến xa hơn: nó tuyên bố những gì có thể thay đổi một cách hợp pháp sau khi tài liệu đã được chứng nhận, và một trình xem tuân thủ sẽ cảnh báo nếu bất kỳ điều gì nằm ngoài sự cho phép đó bị chạm vào. Việc đọc mức độ quyền đó cho người kiểm tra biết liệu một tệp có được chứng nhận hay không và nếu có thì nó được định cấu hình để bị khóa chặt chẽ đến mức nào.

Quyền MDP là một số nguyên với ba giá trị được xác định. Mức 1 có nghĩa là hoàn toàn không cho phép thay đổi nào; bất kỳ sửa đổi nào cũng làm hỏng chứng nhận. Mức 2 cho phép điền biểu mẫu và ký tên, trường hợp phổ biến cho một hợp đồng nhằm hoàn thành và ký nhưng không được thay đổi mặt khác. Mức 3 bổ sung thêm cho phép các chú thích trên hết việc điền biểu mẫu và ký tên. Biết được mức độ cho phép logic tiếp nhận của bạn suy luận về ý định: một tài liệu được chứng nhận ở mức 1 nhưng lại mang các trường biểu mẫu hoặc kịch bản là đang tự mâu thuẫn với chính nó, và mâu thuẫn đó rất đáng bị gắn cờ.

Thành phần đọc số lượng chữ ký và hiển thị mỗi chữ ký dưới dạng một bản ghi có trường Permission mang giá trị MDP đó, được lấy trực tiếp từ lệnh gọi FPDFSignatureObj_GetDocMDPPermission bên dưới. Quyền bằng không có nghĩa là chữ ký không phải là một chữ ký chứng nhận (DocMDP), vì vậy không có sự khóa chặt cấp tài liệu nào để báo cáo.

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;

Một cuộc kiểm tra không xác thực mật mã của chữ ký ở đây; việc xác minh chuỗi chứng chỉ là một mối quan tâm riêng biệt. Những gì nó báo cáo là ý định được khai báo: tệp này nói rằng nó đã bị khóa ở mức này. Đó chính xác là ngữ cảnh mà một người phản biện cần để đánh giá xem các thay đổi sau đó, hoặc sự hiện diện đơn thuần của nội dung hoạt động, có nhất quán với cách tác giả đã niêm phong tài liệu hay không.

Phần còn lại của bề mặt: các tệp được nhúng và XFA

Hai mục nữa hoàn thiện một danh mục kiểm kê đầy đủ. Các tệp nhúng là toàn bộ tài liệu được mang theo bên trong PDF dưới dạng tệp đính kèm, và chúng là phương tiện phân phối cổ điển, bởi vì một báo cáo trông có vẻ lành tính có thể gửi kèm một tệp thực thi hoặc một tệp PDF độc hại thứ hai trong cây đính kèm của nó. Thành phần hiển thị số lượng tệp đính kèm và tên của mỗi tệp, vì vậy cuộc kiểm tra có thể liệt kê những gì đang đi kèm mà không cần trích xuất hoặc mở bất kỳ thứ gì trong số đó.

Sự hiện diện của XFA là cờ hiệu còn lại. Một biểu mẫu XFA thay thế AcroForm tĩnh bằng một kiến trúc biểu mẫu dựa trên XML mang mô hình kết xuất và lập kịch bản riêng của nó, một bề mặt lớn hơn và phức tạp hơn so với một biểu mẫu thông thường. Bạn không cần phải xử lý XFA để lưu ý rằng nó ở đó; sự hiện diện đơn thuần của nó là một tín hiệu cho thấy tệp mang một lớp tương tác phong phú hơn đáng để xem xét kỹ hơn. Thành phần báo cáo nó dưới dạng một giá trị boolean duy nhất.

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;

Một quy trình chỉ đọc viết một báo cáo

Kết hợp các mảnh ghép lại với nhau và cuộc kiểm tra là một quy trình duy nhất tải một tài liệu, liệt kê các script và phần thân của chúng, liệt kê các mục tiêu Launch và URI, báo cáo cấp độ MDP chữ ký, ghi chú các tệp đính kèm và XFA, và ghi các phát hiện vào một nhật ký. Nó không kết xuất gì cả, vì vậy nó rẻ và không thể bị lừa hiển thị nội dung trang thù địch. Đầu ra là một bản ghi phẳng, con người có thể đọc được mà một người phản biện hoặc một quy tắc hạ nguồn có thể tác động.

Hình dáng hoạt động tốt trong thực tế là thu thập từng phát hiện dưới dạng một dòng, đặt tiền tố cho những phát hiện thực sự rủi ro để chúng được sắp xếp lên trên cùng của hàng đợi đánh giá, và lưu giữ toàn bộ nội dung bên cạnh tệp. Một tài liệu không có script, không có action launch, không có tệp đính kèm, không có XFA, và hoặc không có chữ ký hoặc chứng nhận nhất quán sẽ trôi qua một cách êm đẹp. Một tài liệu kích hoạt nhiều cờ cùng lúc là tài liệu mà một người nên xem trước khi bất kỳ giai đoạn sau nào thực sự mở nó. Cuộc kiểm tra không đưa ra quyết định tin cậy thay bạn. Nó đảm bảo quyết định được đưa ra dựa trên thông tin đầy đủ chứ không phải mù quáng.

Một khi một tệp vượt qua cuộc kiểm tra và bạn thực sự cần xem nó, hãy làm như vậy dưới sự hạn chế thay vì trong một trình xem mặc định. Cách tiếp cận trong our walkthrough on building a secure PDF preview in Delphi chỉ ra cách giữ cho tính năng tự động xử lý liên kết và nội dung hoạt động không hoạt động trong một lượt xem được kiểm soát. Để gộp việc liệt kê này vào một luồng tiếp nhận hoàn chỉnh với công cụ của người phản biện, hãy xem the PDF intake and review workbench article. Both build on the same read-only, render-free foundation and ship as part of the PDFium Component for Delphi and C++Builder, alongside the rendering, text, form, and signature APIs covered elsewhere on this blog.