การพรีวิว PDF ที่ไม่น่าเชื่อถือภายในแอปพลิเคชันของคุณเองเป็นการตัดสินใจเชิงการทำงาน (execution decision) และส่วนที่สำคัญไม่ใช่หน้าตาของตัวแสดงผล แต่คือสิ่งที่พาเนลนั้นปฏิเสธที่จะทำเองโดยลำพัง อย่าเขียนไฟล์ลงดิสก์ อย่าปล่อยให้ลิงก์ของมัน shell out อย่ายื่น path ให้กับไฟล์แนบของมัน ความเสียหายส่วนใหญ่จากเอกสารที่มีเจตนาร้ายไม่ได้มาจากช่องโหว่ของเอนจิน แต่มาจากตัวแสดงผลที่ทำสิ่งธรรมดาสามัญกับอินพุตที่ผู้โจมตีป้อนให้: เปิดลิงก์ file:// ไปยัง UNC share ที่รั่วไหลข้อมูลรับรอง NTLM ทิ้งสำเนาที่จัดเตรียมไว้ในไดเรกทอรีชั่วคราว คัดลอกเพย์โหลดที่ฝังอยู่ไปยังที่ใดก็ตามที่สตริงชื่อไฟล์บอกให้ทำ PDFium Component เป็นตัวแสดงผล PDF แบบมีซอร์สโค้ดสำหรับ Delphi, C++Builder และ Lazarus และมันวางสวิตช์ที่เกี่ยวข้องไว้ในที่ที่คุณเอื้อมถึงได้: แฟล็กตอนโหลดที่ฆ่าสคริปต์ทิ้ง เหตุการณ์คลิกลิงก์ที่คุณยับยั้งได้ การเข้าถึงไฟล์แนบที่ผ่านโค้ดของคุณเอง และบิตสิทธิ์ที่คุณอ่านได้ ลำดับด้านล่างนี้จะไล่ตามเอกสารตั้งแต่วินาทีที่มันมาถึงจนถึงวินาทีที่ผู้ใช้คลิกอะไรบางอย่างในนั้น
โมเดลภัยคุกคามของพาเนลพรีวิว
ต้องซื่อตรงกับตัวเองว่า "การพรีวิวที่ปลอดภัย" ให้อะไรคุณได้จริง ๆ ตัวเรนเดอร์แยกวิเคราะห์ไบต์ที่ไม่น่าเชื่อถืออยู่ดีไม่ว่าคุณจะทำอะไรก็ตาม และการเสริมความแข็งแกร่งของเอนจินเองคือพื้นที่คุณยืนอยู่ ทุกอย่างที่อยู่เหนือพื้นนั้นคือนโยบายระดับแอปพลิเคชัน: สคริปต์จะเริ่มทำงานหรือไม่ การคลิกลิงก์จะทำอะไร ไฟล์ที่ฝังอยู่จะไปถึงดิสก์ได้หรือไม่ คลิปบอร์ดและเครื่องพิมพ์เป็นประตูหรือกำแพง มีสิ่งหนึ่งที่ควรตัดทิ้งออกจากสมการตั้งแต่เนิ่น ๆ คือสวิตช์ FPDF_SetSandBoxPolicy ของเอนจิน ข้อจำกัดส่วนใหญ่ของเอนจินถูกคอมไพล์ฝังไว้แล้ว สวิตช์นี้แทบไม่เปลี่ยนอะไรในทางปฏิบัติ และการฝากความหวังเรื่อง isolation ไว้กับมันมีแต่จะสร้างความรู้สึกลวงว่าได้ทำอะไรบางอย่างไปแล้ว เมื่ออินพุตมีเจตนาร้ายจริง ๆ เช่นพอร์ทัลอัปโหลดสาธารณะ isolation ที่แท้จริงเพียงอย่างเดียวคือการเรนเดอร์ในโปรเซสสิทธิ์ต่ำที่แยกออกไปต่างหาก แล้วส่ง bitmap ไปให้ UI แฟล็กภายในโปรเซสคือแค่นโยบาย ไม่ใช่การกักกัน (containment)
มีพื้นผิวสองจุดที่ลืมง่ายมาก เพราะไม่มีการคลิกใดแตะต้องมันเลย จุดแรกคือไฟล์ชั่วคราว ถ้าไปป์ไลน์ของคุณจัดเตรียมเอกสารขาเข้าลงดิสก์ก่อนพรีวิว สำเนาที่จัดเตรียมไว้เหล่านั้นจะอยู่รอดเกินอายุของเซสชัน เว้นแต่จะมีอะไรบางอย่างลบมันออกไปได้อย่างยืนยันแน่ชัด และไฟล์ที่ "กู้คืนได้จากไดเรกทอรีชั่วคราว" ก็ได้เอาชนะการควบคุมทุกอย่างที่พาเนลเองบังคับใช้ไปอย่างเงียบ ๆ แล้ว ให้โหลดจากหน่วยความจำผ่าน TPdfStreamAdapter แทน เพื่อไม่ให้ไบต์ที่เป็นอันตรายได้ path เป็นของตัวเองเลย จุดที่สองคือคลิปบอร์ด พรีวิวที่อนุญาตให้เลือกและคัดลอกได้ก็ได้ส่งออกเอกสารไปแล้ว ทีละหน้าจอ และไม่มีการดักลิงก์ใดจะจับเรื่องนั้นได้เลย
ฆ่า JavaScript ตอนโหลด ไม่ใช่ที่ UI
JavaScript ของเอกสารใน PDFium Component จะเริ่มทำงานพร้อมกับสภาพแวดล้อม form-fill เท่านั้น ดังนั้นการโหลดด้วย FormFill := False จึงปิดสคริปต์ตั้งแต่รากเลย แทนที่จะไปกดอาการของมันไว้:
procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
Pdf.FileName := FilePath;
Pdf.FormFill := False; // ไม่มีสภาพแวดล้อมฟอร์ม จึงไม่มีเอนจิน JavaScript
Pdf.Active := True;
FPermissions := Pdf.Permissions; // คำแฟล็กดิบ; ตั้งบิตทั้งหมด = ไม่มีข้อจำกัด
end;
การแลกเปลี่ยนนี้เป็นเรื่องจริงและควรอยู่ในสเปกของคุณ เมื่อปิด form fill การโต้ตอบกับ AcroForm ที่ถูกต้องตามกฎหมายและสคริปต์ตรวจสอบความถูกต้องก็หายไปด้วย ฟิลด์จะเรนเดอร์ด้วย appearance ที่บันทึกไว้ล่าสุดแต่แก้ไขไม่ได้ สำหรับพาเนลพรีวิว นี่มักจะเป็นการตัดสินใจที่ถูกต้อง เพราะพรีวิวหมายถึงการดู ไม่ใช่การกรอก แต่ถ้าหน้าต่างเดียวกันนั้นทำหน้าที่เป็นพื้นผิวสำหรับกรอกฟอร์มสำหรับเอกสารภายในที่เชื่อถือได้ด้วย คำตอบคือต้องมีสองเส้นทางการโหลด พร้อมการตัดสินใจเรื่องความเชื่อถือได้ที่ชัดเจนคั่นกลาง ไม่ใช่เส้นทางเดียวที่ตั้งค่าประนีประนอมซึ่งหลวมเกินไปสำหรับกรณีที่เป็นอันตราย และแน่นเกินไปสำหรับกรณีที่เชื่อถือได้ ฝั่งการกรอกฟอร์มของการแยกนี้มีกับดักของตัวเองอยู่ ซึ่งครอบคลุมอยู่ในการไล่ลำดับฟิลด์ฟอร์มและการสร้าง appearance ใหม่
ลิงก์: ตัวจัดการเริ่มต้นจะ shell out
ถ้าปล่อยไว้เฉย ๆ การคลิกลิงก์จะส่งตรงไปยังระบบปฏิบัติการ LinkOptions เริ่มต้นของตัวแสดงผลมี loAutoOpenURI รวมอยู่ด้วย ซึ่งเป็นการรั่วไหลแบบ file:// ไปยัง UNC share ที่รอเวลาจะเกิดขึ้น มีเหตุการณ์สองตัวที่เป็นจุดคอขวด: OnWebLinkClick สำหรับ URL ที่ตรวจพบในข้อความของหน้า และ OnAnnotationLinkClick สำหรับคำอธิบายประกอบแบบลิงก์ที่แบก URI หรือ launch action มาด้วย ให้ตั้ง Handled := True ในทั้งสองตัว แบบไม่มีเงื่อนไข ก่อนที่จะตัดสินใจอะไรทั้งนั้น แล้วค่อยอนุญาตกลับเฉพาะสิ่งที่นโยบายยอมให้เท่านั้น เป็นชั้นที่สอง ให้ตัด loAutoOpenURI ออกจาก LinkOptions สำหรับอินพุตที่เป็นอันตราย และให้แน่ใจว่า loAutoLaunch ซึ่งปิดอยู่โดยค่าเริ่มต้น จะไม่มีวันคืบคลานกลับมาผ่านการคัดลอกค่าคอนฟิกเก่า:
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
const Url: WString; var Handled: Boolean);
begin
Handled := True; // อย่าปล่อยให้ตกไปยังพฤติกรรม shell เริ่มต้นเด็ดขาด
if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
and HostIsAllowed(Url) then
OpenInBrowser(Url)
else
FAudit.LogBlockedLink(FDocumentId, Url);
end;
มีสองรายละเอียดที่ตัดสินว่าแนวทางนี้จะยึดอยู่ได้จริงหรือไม่ ข้อแรก การตรวจสอบ scheme ต้องเป็นการตรวจสอบ prefix บนสตริงดิบก่อนการแยกวิเคราะห์ใด ๆ ทั้งสิ้น เพราะ file:// UNC path และ scheme แปลก ๆ คือค่าที่ทำให้ URL parser แบบไร้เดียงสาพังพอดี หรือหลุดรอดผ่าน parser ที่ normalize มากเกินไป ข้อสอง ให้บันทึกทุกการบล็อกพร้อมแนบตัวตนของเอกสารไว้ด้วย ลิงก์ file:// ที่ถูกบล็อกไม่กี่ครั้งเป็นแค่สัญญาณรบกวนพื้นหลัง แต่การเกิดขึ้นเป็นชุดข้ามเอกสารขาเข้าจำนวนมากในช่วงเวลาสั้น ๆ คือเหตุการณ์ที่ทีมความปลอดภัยของคุณอยากได้ยินจากคุณมากกว่าจะไปได้ยินจากที่อื่น
ไฟล์แนบ: นโยบายนามสกุลไฟล์ และชื่อไฟล์ที่คุณไม่ได้เป็นคนเลือก
PDF เป็นคอนเทนเนอร์ และ AttachmentCount พร้อมพร็อพเพอร์ตี้ AttachmentName[] จะบอกคุณว่ามันแบกอะไรมาก่อนที่จะมีอะไรแตะดิสก์เลย มีการควบคุมสองอย่างแยกกันที่สำคัญตรงนี้ และมีแค่อย่างเดียวที่เห็นได้ชัด อย่างที่ชัดเจนคือนโยบายเรื่องประเภท: allowlist ของนามสกุลไฟล์ที่อนุญาตให้ส่งออกได้ อย่างที่ละเอียดอ่อนกว่าคือชื่อของไฟล์แนบเป็นข้อมูลที่ผู้โจมตีควบคุมได้เต็มที่ จบ ชื่อที่ฝังไว้แบบ ..\..\Startup\update.exe จะเปลี่ยนการ save แบบไม่ระวังตัวให้กลายเป็น path traversal ที่ทิ้ง executable ลงในโฟลเดอร์ที่ Windows รันตอน login คอมโพเนนต์ส่งเพย์โหลดให้คุณเป็นไบต์ผ่าน Attachment[] และปล่อยให้โค้ดของคุณเลือก path เอง ดังนั้นให้สร้าง path นั้นจาก basename ที่ผ่านการทำความสะอาดแล้ว และห้ามสร้างจากสตริงที่ฝังไว้แบบดิบเด็ดขาด:
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
RawName, SafeName, Ext: string;
Data: TBytes;
begin
RawName := string(Pdf.AttachmentName[Index]);
SafeName := ExtractFileName(RawName); // ตัดส่วนประกอบของ path ออกทั้งหมด
Ext := LowerCase(ExtractFileExt(SafeName));
if not FAllowedExt.Contains(Ext) then // เป็น allowlist ไม่ใช่ blocklist
raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);
Data := Pdf.Attachment[Index]; // เพย์โหลดที่ฝังไว้ในรูปไบต์ดิบ
TFile.WriteAllBytes(
IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;
ให้เลือกทางของ allowlist ดีกว่า blocklist ของนามสกุล "อันตราย" คือการแข่งที่คุณจะแพ้ในวันที่มีใครสักคนเอานามสกุลที่คุณไม่เคยได้ยินมาก่อนมาใช้เป็นอาวุธ ในขณะที่ allowlist ของ .pdf, .png และ .csv จะล้มเหลวแบบปิดกั้นไว้ก่อน (fail closed)
สิทธิ์การเข้ารหัสรับประกันอะไรได้จริง ๆ
security handler มาตรฐานของ ISO 32000-1 เข้ารหัสแฟล็กสิทธิ์สำหรับการพิมพ์ การคัดลอกเนื้อหา และการแก้ไข และพร็อพเพอร์ตี้ Permissions กับ UserPermissions จะเปิดเผยแฟล็กเหล่านั้นเป็น bitmask ดิบเมื่อเอกสารเปิดขึ้นแล้ว ISO 32000-1 ตารางที่ 22 กำหนดความหมายของแต่ละบิตไว้ และไฟล์ที่ไม่ได้เข้ารหัสจะรายงานว่าตั้งบิตทุกบิตไว้หมด ให้อ่านค่าเหล่านี้และเคารพมันในชั้นคำสั่งของคุณ แต่ต้องเข้าใจให้ชัดว่ามันคืออะไรกันแน่ สำหรับเอกสารที่เข้ารหัสด้วย owner password พร้อม user password ที่ว่างเปล่า เนื้อหาจะถูกถอดรหัสเต็มรูปแบบตอนเปิด และแฟล็กเหล่านั้นเป็นแค่คำขอไปยังตัวแสดงผลที่ทำตามมาตรฐาน ไม่ใช่กลไกบังคับใช้ เรื่องนี้มีผลตามมาสองอย่างที่ดึงไปคนละทิศทาง อย่างแรกคือห้ามนำเสนอแฟล็กสิทธิ์ต่อผู้ใช้ว่าเป็นคุณสมบัติด้านความปลอดภัยของเอกสารที่พวกเขาได้รับ เพราะมันไม่ใช่แบบนั้น ในขณะเดียวกัน ให้เคารพบิตการดึงข้อมูลเพื่อการช่วยการเข้าถึง (bit 10) แม้ในกรณีที่การคัดลอกทั่วไป (bit 5) ถูกปฏิเสธก็ตาม การเข้าถึงของโปรแกรมอ่านหน้าจอถูกแยกไว้ต่างหากในโมเดลสิทธิ์โดยตั้งใจ และการตัดมันทิ้งเพราะ "การคัดลอกถูกปิด" จะทำให้เทคโนโลยีช่วยการเข้าถึงพังโดยไม่ได้ประโยชน์ด้านความปลอดภัยอะไรเลย
ให้บังคับใช้การกระทำที่ถูกปฏิเสธที่ระดับคำสั่ง ไม่ใช่ด้วยการซ่อนปุ่มบนทูลบาร์ Ctrl+C, เมนูคลิกขวา และการลากเพื่อเลือกล้วนข้ามทูลบาร์ไปได้ทั้งหมด แต่การตรวจสอบสิทธิ์เพียงจุดเดียวภายในคำสั่งคัดลอกจะไม่มีอะไรข้ามผ่านไปได้เลย
สำหรับเอกสารที่ต้องใช้ user password จริง ๆ ให้กำหนดค่า Password ก่อน Active := True และปฏิบัติต่อค่านั้นเหมือนเป็นความลับจริง ๆ: ดึงมันมาจาก credential store ของคุณต่อเซสชัน อย่าให้มันหลุดเข้าไปใน log หรือรายงาน crash และห้ามเก็บมันไว้ถาวรข้าง ๆ เอกสารเด็ดขาด พาเนลพรีวิวที่แคชรหัสผ่านไว้ "เพื่อความสะดวก" ได้กลายเป็นฐานข้อมูลรหัสผ่านไปอย่างเงียบ ๆ โดยไม่มีการป้องกันใด ๆ ของฐานข้อมูลรหัสผ่านที่แท้จริงเลย
การพิมพ์สมควรมีการตัดสินใจของตัวเองต่างหาก แทนที่จะรับช่วงต่อจากกฎการคัดลอกไปเลยดื้อ ๆ สิ่งพิมพ์จริงไม่ถูกตรวจสอบได้โดยธรรมชาติของมันเอง แต่การบล็อกการพิมพ์แบบเด็ดขาดมักผลักให้ผู้ใช้หันไปใช้การแคปหน้าจอแทน ซึ่งแย่กว่าในทุกมิติ ทางสายกลางที่ใช้กันทั่วไปคืออนุญาตให้พิมพ์ได้ แต่ประทับตัวตนของผู้ใช้และเวลาไว้บนทุกหน้า บังคับใช้ภายในคำสั่งพิมพ์ แค่ต้องตั้งความคาดหวังที่ถูกต้องไว้กับมัน: ลายน้ำคือการยับยั้งและการระบุตัวตน ไม่ใช่การป้องกันไม่ให้เกิดขึ้น
สิ่งที่การตรวจรับไฟล์ขาเข้าควรบอกคุณไว้ล่วงหน้าแล้ว
พาเนลพรีวิวจะตัดสินใจได้ดีกว่าเมื่อไฟล์มาถึงพร้อมกับสำนวนข้อมูลที่แนบมาด้วยแล้ว: เข้ารหัสหรือไม่ มี JavaScript หรือไม่ สำมะโนไฟล์แนบ ประเภทของฟอร์ม การตรวจสอบขั้นนั้นควรอยู่ในขั้นตอนก่อนตัวแสดงผล และรูปแบบที่อธิบายไว้ในการสร้างเวิร์กเบนช์ตรวจรับ PDF ขาเข้าผลิตแฟล็กที่นโยบายพรีวิวต้องการใช้ออกมาได้พอดี ไฟล์ที่การตรวจรับติดป้ายว่ามีความเสี่ยงจะเปิดผ่านเส้นทางที่เสริมความแข็งแกร่งไว้โดยอัตโนมัติ ส่วนเอกสารทั่วไปยังคงความสะดวกไว้ตามเดิม ให้ผูกทั้งสองขั้นตอนเข้ากับออบเจกต์นโยบายที่ใช้ร่วมกันตัวเดียว แทนที่จะเป็นหน้าจอคอนฟิกสองหน้าจอ ซึ่งจะเริ่มเพี้ยนออกจากกันตั้งแต่รีลีสที่สองไม่ว่าคุณจะเขียนมันอย่างระมัดระวังแค่ไหนในครั้งแรก
เส้นแบ่งระหว่าง in-process กับ out-of-process อยู่ตรงไหนขึ้นอยู่กับว่าใครส่งไฟล์มาให้คุณ สำหรับการรับไฟล์ทางธุรกิจปกติ คนที่ส่งเอกสารมาเป็นที่รู้จักและแค่ไม่ระมัดระวังเท่านั้น การพรีวิวแบบ in-process ที่ปิดสคริปต์และดักลิงก์ไว้เป็นมาตรฐานที่อธิบายได้ แต่สำหรับการอัปโหลดสาธารณะแบบไม่ระบุตัวตน มันไม่ใช่แบบนั้น และการตั้งแฟล็ก in-process มากแค่ไหนก็ไม่ทำให้มันกลายเป็นแบบนั้นได้ ให้เรนเดอร์ไฟล์เหล่านั้นในเวิร์กเกอร์สิทธิ์ต่ำที่แยกออกไปต่างหาก แล้วส่ง bitmap ไปให้ UI เพื่อให้ข้อบกพร่องของเอนจินทำให้คุณเสียแค่เวิร์กเกอร์ตัวเดียว ไม่ใช่แอปพลิเคชันหลัก ให้ตัดสินใจเรื่องการแยกนี้อย่างตั้งใจ แล้วบันทึกไว้ว่าเส้นทางการรับไฟล์แต่ละเส้นทางตกอยู่ในกลุ่มไหน เพราะต้นทุนของการเดาผิดนั้นไม่สมมาตรกัน
เรื่องไลเซนส์ พื้นผิว API ที่เกี่ยวข้องกับความปลอดภัย และดีโมตัวแสดงผลที่เสริมความแข็งแกร่งแล้วอยู่บนหน้าผลิตภัณฑ์: PDFium Component