Bài viết kỹ thuật

Xem trước PDF an toàn trong ứng dụng Delphi bằng PDFium Component

Xem trước một PDF không đáng tin trong ứng dụng của chính bạn là một quyết định về thực thi, và phần quan trọng không phải là giao diện của trình xem mà là những gì khung xem từ chối tự làm. Đừng ghi file ra đĩa. Đừng để các liên kết của nó gọi ra shell. Đừng đưa cho các tệp đính kèm của nó một đường dẫn. Phần lớn thiệt hại từ một tài liệu độc hại không đến từ một lỗ hổng khai thác trong engine mà từ việc trình xem làm những việc hoàn toàn bình thường với đầu vào do kẻ tấn công cung cấp: mở một liên kết file:// đến một chia sẻ UNC làm rò rỉ thông tin xác thực NTLM, để lại một bản sao tạm trong thư mục temp, sao chép các payload nhúng đến bất cứ đâu mà một chuỗi tên file bảo nó. PDFium Component là một trình xem PDF có mã nguồn cho Delphi, C++Builder và Lazarus, và nó đặt các công tắc liên quan ở nơi bạn có thể với tới: một cờ lúc tải làm chết scripting, các sự kiện nhấp liên kết bạn có thể phủ quyết, quyền truy cập tệp đính kèm chạy qua mã của chính bạn, và các bit quyền bạn có thể đọc. Trình tự dưới đây theo dõi một tài liệu từ khoảnh khắc nó đến cho tới khoảnh khắc người dùng nhấp vào thứ gì đó trong đó

Mô hình mối đe dọa của một khung xem trước

Hãy trung thực về những gì "xem trước an toàn" thực sự mang lại cho bạn. Bộ render phân tích byte không đáng tin cậy dù bạn có làm gì đi nữa, và việc gia cố riêng của engine là sàn nhà bạn đang đứng trên đó. Mọi thứ bên trên sàn nhà đó là chính sách ứng dụng: liệu script có được khởi tạo hay không, một cú nhấp liên kết làm gì, liệu file nhúng có thể chạm tới đĩa hay không, liệu clipboard và máy in là cửa hay tường. Một điều nên gạch bỏ sớm là công tắc FPDF_SetSandBoxPolicy của engine. Hầu hết các giới hạn của engine đã được biên dịch sẵn, công tắc đó thay đổi rất ít trong thực tế, và việc dựa vào nó cho bất kỳ phần nào trong câu chuyện cô lập của bạn chỉ tạo ra cảm giác sai lầm rằng bạn đã làm được điều gì đó. Khi đầu vào thực sự thù địch, chẳng hạn như một cổng tải lên công khai, sự cô lập thực sự duy nhất là render trong một tiến trình đặc quyền thấp riêng biệt và gửi các bitmap tới giao diện. Các cờ trong tiến trình là chính sách. Chúng không phải là sự ngăn chặn

Sơ đồ PDFium Component đối lập các công tắc chính sách preview PDF trong tiến trình với render ngoài tiến trình trong một worker đặc quyền thấp cho các tài liệu thù địch
Các cờ trong-tiến trình nâng hàng rào cho người gửi đã biết, trong khi bản tải lên ẩn danh biện minh cho một worker đặc quyền thấp riêng chỉ chuyển bitmap tới UI

Hai bề mặt dễ bị quên chính xác vì không cú nhấp nào từng chạm vào chúng. Cái đầu tiên là các file tạm. Nếu pipeline của bạn lưu tạm các tài liệu đến ra đĩa trước khi xem trước, những bản sao được lưu tạm đó tồn tại lâu hơn phiên làm việc trừ khi có thứ gì đó xóa chúng một cách có thể kiểm chứng, và một file "có thể khôi phục từ thư mục temp" đã âm thầm đánh bại mọi biện pháp kiểm soát mà chính khung xem thực thi. Thay vào đó hãy tải từ bộ nhớ thông qua TPdfStreamAdapter, để byte thù địch không bao giờ có đường dẫn riêng của chúng. Cái thứ hai là clipboard. Một bản xem trước cho phép chọn-và-sao-chép đã xuất khẩu tài liệu rồi, mỗi lần một màn hình, và không có sự chặn liên kết nào bắt được điều đó

Giết JavaScript lúc tải, không phải trong giao diện

JavaScript tài liệu trong PDFium Component chỉ khởi tạo cùng với môi trường form-fill. Do đó, tải với FormFill := False sẽ tắt scripting ngay từ gốc thay vì chỉ đàn áp các triệu chứng của nó:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // không có môi trường form, do đó không có engine JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // từ cờ thô; tất cả bit bật = không giới hạn
end;

Sự đánh đổi này là có thật và thuộc về đặc tả của bạn. Với form fill bị tắt, tương tác AcroForm hợp lệ và các script xác thực cũng biến mất; các trường hiển thị với hình thức đã lưu lần cuối nhưng không thể chỉnh sửa được. Đối với một khung xem trước, đó thường là quyết định đúng, vì xem trước nghĩa là nhìn, không phải điền. Nhưng nếu cùng một cửa sổ đó cũng đóng vai trò bề mặt điền form cho các tài liệu nội bộ đáng tin cậy, câu trả lời là hai đường tải với một quyết định tin cậy rõ ràng ở giữa, chứ không phải một đường duy nhất với một thiết lập thỏa hiệp quá lỏng lẻo cho trường hợp thù địch và quá chặt cho trường hợp đáng tin. Phía điền form của sự phân chia đó có những cạm bẫy riêng, được đề cập trong điều hướng trường form và tái tạo hình thức

Liên kết: trình xử lý mặc định gọi ra shell

Nếu để yên, các cú nhấp liên kết đi thẳng đến hệ điều hành. LinkOptions mặc định của trình xem bao gồm loAutoOpenURI, chính là lỗ rò rỉ file:// đến chia sẻ UNC đang chờ xảy ra. Hai sự kiện tạo thành điểm nghẽn: OnWebLinkClick cho các URL được phát hiện trong văn bản trang, và OnAnnotationLinkClick cho chú thích liên kết mang hành động URI hoặc khởi chạy. Đặt Handled := True ở cả hai, một cách vô điều kiện, trước khi quyết định bất cứ điều gì, sau đó chỉ cho phép lại những gì chính sách cho phép. Như một lớp thứ hai, hãy bỏ loAutoOpenURI khỏi LinkOptions đối với đầu vào thù địch và đảm bảo loAutoLaunch, vốn tắt theo mặc định, không bao giờ lén quay lại qua một file cấu hình được sao chép:

Sơ đồ luồng chặn cú nhấp liên kết PDF trong một ô preview Delphi với phép kiểm tra tiền tố scheme dạng chuỗi thô và ghi log kiểm toán các liên kết bị chặn
Đặt Handled trong cả hai sự kiện liên kết giữ mọi cú nhấp dưới chính sách ứng dụng, và một phép kiểm tra tiền tố trên chuỗi thô ngăn các scheme file:// và UNC
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // không bao giờ rơi vào hành vi shell mặc định

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

Hai chi tiết quyết định việc này có thực sự đứng vững hay không. Thứ nhất, việc kiểm tra scheme phải là kiểm tra tiền tố trên chuỗi thô trước bất kỳ phân tích cú pháp nào, bởi vì file://, đường dẫn UNC, và các scheme kỳ lạ chính xác là những giá trị làm crash một bộ phân tích URL ngây thơ hoặc lọt qua một bộ chuẩn hóa quá hăng hái. Thứ hai, ghi log mọi lần chặn kèm theo danh tính tài liệu. Một vài liên kết file:// bị chặn chỉ là nhiễu nền; một loạt liên kết như vậy trên nhiều tài liệu đến trong một khoảng thời gian ngắn là một sự cố mà đội bảo mật của bạn thà nghe từ bạn còn hơn từ nơi khác

Tệp đính kèm: chính sách phần mở rộng và tên file bạn không chọn

PDF là một định dạng chứa đựng, và AttachmentCount cùng thuộc tính AttachmentName[] cho bạn biết nó mang gì trước khi bất cứ thứ gì chạm tới đĩa. Hai kiểm soát riêng biệt quan trọng ở đây, và chỉ một trong số đó là hiển nhiên. Cái hiển nhiên là chính sách loại: một danh sách cho phép các phần mở rộng được phép xuất khẩu. Cái tinh tế là tên của tệp đính kèm là dữ liệu do kẻ tấn công kiểm soát, chấm hết. Một tên nhúng như ..\..\Startup\update.exe biến một lệnh lưu bất cẩn thành một cuộc tấn công path traversal đặt một file thực thi vào một thư mục mà Windows chạy khi đăng nhập. Component trao cho bạn payload dưới dạng byte thông qua Attachment[] và để mã của bạn chọn đường dẫn, vì vậy hãy xây dựng đường dẫn đó từ một tên cơ sở đã được làm sạch và không bao giờ từ chuỗi nhúng thô:

Sơ đồ pipeline PDFium Component làm sạch một tên tệp đính kèm PDF do kẻ tấn công kiểm soát qua ExtractFileName và một danh sách cho phép phần mở rộng trước khi ghi byte
Tên attachment nhúng là đầu vào của kẻ tấn công, nên đường export được dựng lại từ một basename đã làm sạch và được gác bởi một danh sách trắng phần mở rộng fail-closed
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // loại bỏ mọi thành phần đường dẫn
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // danh sách cho phép, không phải danh sách chặn
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // payload nhúng dưới dạng byte thô
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Hãy ưu tiên hướng danh sách cho phép. Một danh sách chặn các phần mở rộng "nguy hiểm" là một cuộc đua bạn sẽ thua vào ngày ai đó vũ khí hóa một phần mở rộng mà bạn chưa từng nghe tới; một danh sách cho phép gồm .pdf, .png, và .csv thất bại theo hướng đóng an toàn

Quyền mã hóa thực sự hứa hẹn điều gì

Trình xử lý bảo mật chuẩn của ISO 32000-1 mã hóa các cờ quyền cho in ấn, sao chép nội dung, và sửa đổi, và các thuộc tính PermissionsUserPermissions phơi bày chúng dưới dạng mặt nạ bit thô ngay khi tài liệu mở ra. Bảng 22 của ISO 32000-1 định nghĩa các bit đó, và một file không mã hóa báo cáo mọi bit đều bật. Hãy đọc chúng và tôn trọng chúng ở lớp lệnh của bạn, nhưng phải rõ ràng về việc chúng thực sự là gì. Đối với một tài liệu được mã hóa bằng mật khẩu chủ sở hữu và mật khẩu người dùng rỗng, nội dung giải mã hoàn toàn khi mở, và các cờ đó là một yêu cầu gửi tới các trình xem tuân thủ, không phải một cơ chế thực thi. Điều đó có hai hệ quả, và chúng kéo theo hai hướng ngược nhau. Không bao giờ trình bày các cờ quyền cho người dùng như một thuộc tính bảo mật của tài liệu họ nhận được, bởi vì chúng không phải vậy. Đồng thời, hãy tôn trọng bit trích xuất cho khả năng tiếp cận (bit 10) ngay cả khi việc sao chép chung (bit 5) bị từ chối; quyền truy cập của trình đọc màn hình được tách riêng có chủ đích trong mô hình quyền, và việc loại bỏ nó vì "sao chép đang bị tắt" phá hỏng công nghệ hỗ trợ mà không mang lại lợi ích bảo mật nào

Hãy thực thi các hành động bị từ chối ở cấp độ lệnh, không phải bằng cách ẩn các nút thanh công cụ. Ctrl+C, menu ngữ cảnh, và chọn-kéo đều bỏ qua một thanh công cụ; một phép kiểm tra quyền duy nhất bên trong lệnh sao chép sẽ không bị bỏ qua bởi bất cứ thứ gì

Đối với các tài liệu thực sự yêu cầu mật khẩu người dùng, hãy gán Password trước Active := True và đối xử với giá trị đó như một bí mật đúng nghĩa: lấy nó từ kho lưu trữ thông tin xác thực của bạn theo từng phiên, giữ nó tránh xa log và báo cáo crash, và không bao giờ lưu nó cạnh tài liệu. Một khung xem trước cache mật khẩu "để tiện lợi" đã âm thầm trở thành một cơ sở dữ liệu mật khẩu mà không có bất kỳ biện pháp bảo vệ nào của một cơ sở dữ liệu thực thụ

In ấn xứng đáng có quyết định riêng của nó thay vì thừa hưởng bất cứ điều gì quy tắc sao chép đã đưa ra. Một bản in vật lý theo định nghĩa là không được kiểm toán, nhưng việc chặn in ấn hoàn toàn có xu hướng đẩy người dùng về phía chụp màn hình, thứ tệ hơn trên mọi phương diện. Một điểm trung dung phổ biến là cho phép in nhưng đóng dấu mỗi trang với danh tính người dùng và một dấu thời gian, được thực thi bên trong lệnh in. Chỉ cần giữ đúng kỳ vọng cho nó: một watermark là sự răn đe và quy trách nhiệm. Nó không phải là sự ngăn chặn

Những gì khâu tiếp nhận lẽ ra đã phải báo cho bạn biết

Một khung xem trước đưa ra quyết định tốt hơn khi file xuất hiện với một hồ sơ đã đính kèm sẵn: có mã hóa hay không, có JavaScript hay không, một bản kê tệp đính kèm, loại form. Lượt kiểm tra đó thuộc về phía trước trình xem, và mẫu hình trong xây dựng một workbench kiểm tra tiếp nhận PDF tạo ra chính xác các cờ mà một chính sách xem trước muốn tiêu thụ. Các file mà khâu tiếp nhận đánh dấu là rủi ro sẽ tự động mở qua đường được gia cố; các tài liệu thông thường giữ nguyên các tiện ích của chúng. Hãy liên kết hai giai đoạn đó vào một đối tượng chính sách dùng chung thay vì hai màn hình cấu hình, vốn sẽ trôi dạt xa nhau vào lần phát hành thứ hai dù bạn viết chúng cẩn thận đến đâu ở lần đầu tiên

Ranh giới giữa trong-tiến-trình và ngoài-tiến-trình rơi ở đâu phụ thuộc vào việc ai gửi file cho bạn. Đối với việc tiếp nhận kinh doanh thông thường, những người gửi tài liệu là đã biết và chỉ đơn thuần bất cẩn, và xem trước trong tiến trình với scripting tắt và liên kết bị chặn là một ngưỡng có thể biện minh được. Đối với tải lên công khai ẩn danh thì không, và không có lượng thiết lập cờ trong tiến trình nào khiến nó trở nên như vậy; hãy render những thứ đó trong một worker đặc quyền thấp riêng biệt và gửi bitmap tới giao diện, để một lỗi trong engine chỉ khiến bạn mất một worker chứ không phải ứng dụng chủ. Hãy quyết định sự phân chia đó một cách có chủ đích và ghi lại mỗi đường tiếp nhận thuộc vào nhóm nào, bởi vì cái giá của việc đoán sai là bất đối xứng

Việc cấp phép, bề mặt API liên quan đến bảo mật, và một demo trình xem được gia cố nằm trên trang sản phẩm: PDFium Component