💡 ตัวอย่างการทำงานเต็มที่พร้อมใช้งานบน GitHub:
load-untrusted-documents-safely-python

วิธีเดิมทำให้เจ็บปวด

คุณเขียนเพียงสามบรรทัดเพื่อสร้างภาพย่อของเอกสารที่อัปโหลดขึ้นมา โค้ดดูเหมือนนี้และดูดีพอสมควร:

with signature.Signature(upload_path) as sign:
    save_page_preview(sign, thumbnail_path)

สิ่งที่บรรทัดเหล่านั้นทำ ก่อนหน้า GroupDocs.Signature 26.9 คือการดึงทุกที่อยู่ที่เอกสารอ้างอิง ไฟล์ Word สามารถเก็บรูปภาพที่ไม่ได้อยู่ในไฟล์ได้ — ไฟล์จะเก็บ URL และสิ่งใดก็ตามที่เปิดไฟล์นั้นก็จะดาวน์โหลด URL นั้น บนเดสก์ท็อปนี่เป็นคุณลักษณะหนึ่ง แต่บนเซิร์ฟเวอร์ที่รับอัปโหลด หมายความว่าผู้ส่งไฟล์ให้คุณเป็นผู้กำหนดว่าที่อยู่ใดบ้างที่โครงสร้างพื้นฐานของคุณจะร้องขอ

การโจมตีนี้มีชื่อว่า server‑side request forgery (SSRF) และมีรูปแบบสามแบบที่ควรอธิบาย ที่อยู่ภายในที่ไม่สามารถเข้าถึงจากอินเทอร์เน็ตได้สามารถเข้าถึงได้จากเซิร์ฟเวอร์ของคุณ ดังนั้นเอกสารที่จัดทำขึ้นอย่างประณีตสามารถทำให้บริการของคุณดึง http://169.254.169.254/ หรือ endpoint ผู้ดูแลระบบบน localhost ได้ เส้นทาง UNC สามารถกระตุ้นโฮสต์ Windows ให้ทำการยืนยันตัวตนออกไป ส่งข้อมูลประจำตัวให้กับเซิร์ฟเวอร์ที่โจมตีควบคุมได้ และลิงก์ไปยังโฮสต์ที่ไม่ตอบสนองเลยจะทำให้เธรดการโหลดค้างจนหมดเวลา ซึ่งเป็นวิธีราคาถูกในการทำให้พูลของ worker ถูกใช้จนเต็มด้วยเอกสารที่ดูเหมือนไร้อันตราย

ไม่มีอะไรในนั้นเป็นบั๊กของไลบรารีเอกสาร การตามลิงก์เป็นสิ่งที่ฟอร์แมตต้องการ ส่วนที่ทำให้ไม่สบายใจคือการทำตามเป็นค่าเริ่มต้นในโค้ดที่ไม่มีใครจะชี้ให้เห็นในการตรวจสอบ

มีวิธีที่ดีกว่า

การโหลดเอกสารอย่างปลอดภัยเป็นพฤติกรรมของ GroupDocs.Signature สำหรับ Python ที่ปฏิเสธการทำคำขอเหล่านั้น ตั้งแต่เวอร์ชัน 26.9, LoadOptions.skip_external_resources มีค่าเริ่มต้นเป็น True ดังนั้นสามบรรทัดเดียวกันจึงไม่ดึงอะไรเลยและจะแสดงตัวแทนที่ตำแหน่งของรูปภาพที่เชื่อมโยง

การเปลี่ยนแปลงนี้เป็นค่าเริ่มต้น ไม่ใช่ฟีเจอร์ใหม่ — คุณสมบัตินี้มีอยู่แล้ว สิ่งที่ 26.9 แก้ไขคือทิศทางที่มันชี้เมื่อโค้ดของคุณไม่ได้ระบุค่าใด ๆ ซึ่งเป็นการตั้งค่าเดียวที่บริการส่วนใหญ่ใช้เสมอ

วิธีใหม่: สามโหมดการโหลด

ขั้นตอนที่ 1 - รักษาค่าตั้งต้นสำหรับสิ่งที่ไม่เชื่อถือ

ไม่มี LoadOptions เลย:

with signature.Signature(source_path) as sign:
    return save_page_preview(sign, preview_path)

ไม่มีการร้องขอใด ๆ ตัวอย่างจะเล็กกว่าที่จะเป็นหากไม่มีการดึงข้อมูล และความแตกต่างของขนาดนี้เป็นหลักฐานที่สะดวกที่สุดที่แสดงว่าไม่มีคำขอใดออกจากเครื่อง

ขั้นตอนที่ 2 - ทำรายการขาว (whitelist) โฮสต์ที่คุณเป็นเจ้าของจริง

เอกสารจำนวนมากเชื่อมโยงไปยังที่ที่น่าเชื่อถือ เช่น CDN ของบริษัท, เซิร์ฟเวอร์รูปภาพภายใน, หรือร้านเทมเพลต ให้อนุญาตเฉพาะนั้นและไม่มีอย่างอื่น:

load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]

with signature.Signature(source_path, load_options) as sign:
    return save_page_preview(sign, preview_path)

กฎการจับคู่ต้องให้ความสนใจ มันเป็นการทดสอบสตริงย่อยโดยไม่คำนึงถึงตัวพิมพ์ใหญ่‑เล็กต่อที่อยู่ของทรัพยากร ซึ่งทำให้ส่วนสั้น ๆ กลายเป็นอันตรายได้: github จะตรงกับ github.attacker.example/payload.png เช่นเดียวกับโฮสต์ที่คุณตั้งใจใช้ ใช้ scheme, host และ path — ตัวอย่างนี้ทำรายการขาว raw.githubusercontent.com/groupdocs-signature/

ขั้นตอนที่ 3 - อนุญาตทุกอย่างโดยเจตนา

พฤติกรรมก่อน 26.9 ที่ยังคงใช้ได้:

load_options = LoadOptions()
load_options.skip_external_resources = False

เหมาะสมสำหรับเอกสารที่แอปพลิเคชันของคุณสร้างเอง มีกับดักหนึ่ง: คุณสมบัติ load_external_resources ที่ล้าสมัยมีขั้วตรงกันข้าม ดังนั้น skip_external_resources = False คือสิ่งที่แทนที่ load_external_resources = True การคัดลอกค่าจากคุณสมบัติเก่าไปจะทำให้ท่าทีความปลอดภัยของคุณกลับด้านโดยไม่มีข้อผิดพลาดบอกเตือน

เปรียบเทียบ: ก่อนและหลัง

เอกสารเดียวกัน, เส้นทางโค้ดเดียวกัน, สามนโยบายการโหลด นี่คือขนาดของไฟล์ที่อยู่ในโฟลเดอร์ Result/ ของตัวอย่าง เพื่อให้ตรวจสอบได้แทนการเชื่อถือโดยตาเปล่า:

โหมดการโหลด ขนาดตัวอย่าง คำขอออก
ค่าเริ่มต้น (26.9 และหลังจากนั้น) 16,435 bytes ไม่มี
โฮสต์ที่ทำรายการขาว 51,738 bytes หนึ่ง, ไปยังที่อยู่ที่อนุญาต
ทรัพยากรทั้งหมด (ค่าเริ่มต้นก่อน 26.9) 51,738 bytes หนึ่งต่อทรัพยากรที่เชื่อมโยง

รูปภาพที่เชื่อมโยงเป็นส่วนต่าง 35,303 bytes ฉันไม่เชื่อถือการตั้งค่านี้จนกว่าจะเห็นตัวเลขสองค่านั้นอยู่เคียงข้างกัน และฉันก็แนะนำให้ทำเช่นเดียวกัน: การอ่านค่าคุณสมบัติกลับบอกคุณว่าคุณตั้งค่าอะไรไว้ ไม่ใช่ว่ากระบวนการทำอะไร

สิ่งที่ถือเป็นทรัพยากรภายนอก?

แคบกว่าที่คนส่วนใหญ่คาดคิด ซึ่งเป็นเหตุผลว่าทำไมการอัปเกรดมักจะไม่ก่อให้เกิดปัญหา รูปภาพที่เชื่อมโยงแทนที่รูปภาพฝัง, ฟิลด์ INCLUDEPICTURE, รูปภาพที่เชื่อมโยงในงานนำเสนอและสเปรดชีต, รวมถึงรูปภาพและสไตล์ชีตที่ SVG อ้างอิง เนื้อหาที่ฝังอยู่จะไม่ถูกแก้ไข เพราะมันอยู่ภายในไฟล์แล้วและไม่ต้องการคำขอใด ๆ เพื่อแสดงผล

ความแตกต่างนี้คือเส้นแบ่งความปลอดภัยทั้งหมด เอกสารจะทำให้เซิร์ฟเวอร์ของคุณออกไปขอข้อมูลได้เฉพาะเมื่อมันเก็บที่อยู่แทนไบต์ ดังนั้นคำถามสำหรับคอร์ปัสใด ๆ ก็คือ มีไฟล์กี่ไฟล์ที่เชื่อมโยงแทนที่ฝัง หากไม่มีเลย ค่าเริ่มต้นใหม่ก็ไม่เสียค่าใช้จ่ายใด ๆ และคุณสามารถอัปเกรดได้โดยไม่ต้องอ่านต่อ

ตัวอย่างจากโลกจริง: การอัปโหลดที่ต้องการการลงลายเซ็น

กรณีที่การเปลี่ยนค่าเริ่มต้นมีไว้เพื่อแก้ไข เอกสารมาจากภายนอกและคุณต้องใส่ลายเซ็นลงไป:

with signature.Signature(source_path) as sign:
    options = QrCodeSignOptions("Approved by GroupDocs.Signature")
    options.encode_type = QrCodeTypes.QR
    options.left = 400
    options.top = 50
    options.width = 120
    options.height = 120

    result = sign.sign(output_path, options)

ไม่มีการร้องขอทรัพยากรภายนอกขณะโหลด, ลงลายเซ็น หรือบันทึกเอกสาร ผลลัพธ์ที่ลงลายเซ็นยังคงรักษาลิงก์ไว้ ดังนั้นผู้ใช้ที่เปิดไฟล์ใน Word ต่อมาจะยังคงเห็นรูปภาพที่แก้ไขบนเครื่องของตนเอง การข้ามขั้นตอนเป็นนโยบายระดับเซิร์ฟเวอร์ ไม่ใช่การแก้ไขเอกสาร — ซึ่งเป็นเหตุผลที่ทำให้มันปลอดภัยต่อไฟล์ที่คุณจัดการในนามของผู้อื่น

สิ่งอื่นที่เปลี่ยนแปลงเมื่อคุณอัปเกรด?

สำหรับบริการส่วนใหญ่ไม่มีอะไรที่มองเห็นได้ ซึ่งควรบอกอย่างชัดเจน เพราะค่าเริ่มต้นด้านความปลอดภัยที่เปลี่ยนพฤติกรรมทั่วทุกที่จะไม่ผ่านการตรวจสอบอัปเกรด การลงลายเซ็น, การตรวจสอบและการค้นหาไม่ได้รับผลกระทบ ข้อยกเว้นคือภาพตัวอย่างที่เคยแสดงรูปภาพที่เชื่อมโยงและตอนนี้แสดงตัวแทน — การเปลี่ยนแปลงทำหน้าที่ของมันแล้ว หากโฮสต์เป็นของคุณให้ทำรายการขาว หากไม่ใช่ให้ปฏิเสธ

ควรชี้ให้เห็นแยกต่างหาก: SVG SVG สามารถอ้างอิงรูปภาพและสไตล์ชีตโดย URL การอ้างอิงเหล่านี้ถือเป็นทรัพยากรภายนอกภายใต้กฎเดียวกัน และ SVG เป็นทั้งฟอร์แมตอัปโหลดที่พบบ่อยและเวกเตอร์ SSRF ที่พบบ่อย บริการที่รับอวาตาร์ SVG แล้วเรนเดอร์บนเซิร์ฟเวอร์เป็นระบบที่การเปลี่ยนแปลงนี้คุ้มครองโดยตรง

รายละเอียด Python หนึ่ง: วิธีการเขียนตัวอย่าง

PreviewOptions รับฟังก์ชันสร้างสตรีมสองตัวแทนการระบุพาธ และ callable ของ Python ธรรมดาก็เพียงพอ:

def create_page_stream(page_data):
    return open(preview_path, "wb")

def release_page_stream(page_data, page_stream):
    page_stream.close()

preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)

หนึ่งฟังก์ชันสร้างสตรีมต่อหน้า อีกฟังก์ชันปล่อยสตรีม ตัวอย่างเอกสารมีหน้าเดียวจึงเขียนไฟล์เดียว; หากเป็นอินพุตหลายหน้า ให้ใส่หมายเลขหน้าในชื่อไฟล์หรือไฟล์แต่ละหน้าจะเขียนทับไฟล์ก่อนหน้า

สรุป

ค่าเริ่มต้นได้ถูกสลับให้พฤติกรรมที่เสี่ยงต้องการการตัดสินใจอย่างชัดเจน ส่วนพฤติกรรมที่ปลอดภัยไม่ต้องทำอะไรเลย รักษาค่าเริ่มต้นสำหรับข้อมูลที่ไม่เชื่อถือ, ทำรายการขาวอย่างแคบเมื่อโฮสต์ของคุณเกี่ยวข้อง, และจำไว้ว่า การลงลายเซ็นไม่เคยต้องใช้เครือข่ายเลย

หากคุณต้องการการตรวจสอบที่เข้มข้นกว่าขนาดไฟล์ ให้ชี้เอกสารทดสอบไปยังโฮสต์ที่คุณควบคุมและดูบันทึกการเข้าถึงขณะรันตัวอย่าง ขนาดไฟล์บอกว่ามีไบต์มาถึงหรือไม่; บันทึกการเข้าถึงบอกว่ามีการร้องขอเกิดขึ้นหรือไม่ และสองอย่างนี้จะแตกต่างกันตรงกรณีที่สำคัญ — โฮสต์ที่ทำรายการขาวแต่ไม่สามารถเข้าถึงได้จะดูเหมือนกับโฮสต์ที่ถูกบล็อกจากผลลัพธ์เพียงอย่างเดียว

การรันตัวอย่างกับเอกสารของคุณเองใช้เวลาประมาณหนึ่งนาทีและบอกคุณในสามขนาดไฟล์ว่าเซอร์วิสของคุณได้ดึงอะไรไปแทนผู้ส่งไฟล์ให้คุณบ้าง

แหล่งข้อมูลเพิ่มเติม