1 คะแนน โดย GN⁺ 2 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • gokrazy/rsync คืออิมพลีเมนต์ rsync แบบมินิมอลที่เขียนด้วย Go และถูกตรวจสอบโดยอิงจากช่องโหว่ rsync 12 รายการที่เปิดเผยในเดือนมกราคม 2025 และพฤษภาคม 2026
  • การตรวจสอบขอบเขต ของ Go และการกำหนดค่าเริ่มต้นเป็น 0 ช่วยเปลี่ยน heap overflow และการรั่วไหลของข้อมูลจากสแตกให้กลายเป็น panic หรือค่าที่ไม่เป็นอันตราย แต่ panic ก็ยังอาจนำไปสู่การปฏิเสธการให้บริการได้
  • กลุ่มปัญหา path traversal และ TOCTOU อาศัย API ไฟล์ที่ต้านทานการ traversal เช่น os.Root ใน Go 1.24 เป็นแนวป้องกันหลัก และ gokrazy/rsync ก็ได้เปลี่ยนมาใช้แนวทางนี้ทั้งหมด
  • กลยุทธ์การทำอิมพลีเมนต์แบบมินิมอลช่วยหลีกเลี่ยงช่องโหว่ที่เกี่ยวข้องกับฟีเจอร์ที่ยังไม่ได้รองรับ เช่น --inc-recursive, การบีบอัด, --safe-links, พร็อกซี และ hostname ACL จึงลดพื้นผิวการโจมตีลง
  • ช่องโหว่ส่วนใหญ่เกิดจาก การตรวจสอบที่ขาดหายไป และความซับซ้อนที่มากเกินจำเป็น และสำหรับกรณีใช้งานที่เรียบง่าย อิมพลีเมนต์ที่เรียบง่ายอาจเหมาะสมกว่า

ภูมิหลังและขอบเขต

  • ในเดือนมกราคม 2025 นักวิจัยด้านความปลอดภัยหลายรายได้เปิดเผย ช่องโหว่ความปลอดภัย 6 รายการ ใน upstream Samba rsync ซึ่งบางรายการเปิดทางให้รันโค้ดตามอำเภอใจและทำให้ไฟล์รั่วไหลได้
  • ประเด็นหลักของการตรวจสอบคือ gokrazy/rsync ซึ่งเป็นอิมพลีเมนต์ที่เข้ากันได้และมีความมินิมอลซึ่งเขียนด้วย Go จะสามารถหลีกเลี่ยงกลุ่มช่องโหว่ลักษณะนี้ได้จริงหรือไม่
  • ขอบเขตการวิเคราะห์ครอบคลุมช่องโหว่ 12 รายการที่รวม ชุดเดือนมกราคม 2025 และชุดเดือนพฤษภาคม 2026 เข้าด้วยกัน
  • หากกำลังรัน upstream rsync ในโปรดักชัน ควรอัปเกรดเป็น 3.4.3 ขึ้นไป และ gokrazy/rsync ควรอัปเกรดเป็น v0.3.3 ขึ้นไป
  • gokrazy/rsync ถูกพัฒนาขึ้นเพื่อใช้ให้บริการแพ็กเกจซอฟต์แวร์ของโปรเจ็กต์วิจัย Linux distribution distri บน router7 ซึ่ง router7 สร้างอยู่บนแพลตฟอร์ม Go appliance อย่าง gokrazy
  • ในช่วงแรกมีเพียง rsync server เท่านั้น แต่ปัจจุบันรองรับได้ทุกทิศทางแล้ว และถูกใช้งานบนเซิร์ฟเวอร์ gokrazy/rsync หลายตัวในฐานะฟังก์ชัน rsync พื้นฐานที่สามารถลิงก์เข้ากับโปรแกรม Go ได้

ช่องโหว่เดือนมกราคม 2025

  • CVE-2024-12084: heap buffer overflow, ความรุนแรง 9.8

    • rsync เปรียบเทียบความยาว checksum ที่ได้รับจากเครือข่ายกับ MAX_DIGEST_LEN แต่โครงสร้างข้อมูลภายในใช้บัฟเฟอร์ขนาด 16 ไบต์ char sum2[SUM_LENGTH] เสมอ
    • MAX_DIGEST_LEN อาจใหญ่ขึ้นได้หากคอมไพล์พร้อมการรองรับ checksum แบบ SHA256 หรือ SHA512 ทำให้ผู้โจมตีสามารถเขียนเกินขอบเขตบัฟเฟอร์ sum2 ได้สูงสุด 48 ไบต์
    • ปัญหานี้ถูกนำเข้ามาใน commit ae16850 เมื่อเดือนกันยายน 2022 ที่เพิ่มการรองรับ checksum แบบ SHA256/SHA512
    • การแก้ไข upstream เปลี่ยน sum2 เป็น sum2_array ที่จัดสรรแบบไดนามิก และแก้ให้จัดสรร/ตรวจสอบตาม xfer_sum_len ซึ่งเป็นความยาว checksum ของอัลกอริทึมการถ่ายโอน
    • ใน Go การตรวจสอบขอบเขตที่ผิดพลาดจะไม่ลามไปเป็น heap buffer overflow แต่จะทำให้เกิด panic จากการตรวจสอบขอบเขตของรันไทม์
    • gokrazy/rsync ก็มีการตรวจสอบ sum header ที่ขาดหายไปเช่นกัน แต่ไม่ใช่ความสับสนเรื่องขนาด และหากเปลี่ยน ChecksumLength เป็น 512 Go runtime จะทำให้เกิด panic: runtime error: slice bounds out of range [:512] with length 16
    • การทำให้เซิร์ฟเวอร์ล่มทั้งตัวไม่ใช่โหมดความล้มเหลวที่พึงประสงค์ จึงได้ เพิ่มการตรวจสอบขอบเขตที่ขาดหายไปเพื่อเปลี่ยน panic ให้เป็น error
  • CVE-2024-12085: การรั่วไหลของข้อมูลบนสแตกเพื่อ bypass ASLR, ความรุนแรง 7.5

    • เนื่องจากมีการตรวจสอบที่ขาดหายไปแบบเดียวกับ CVE-2024-12084 ผู้โจมตีจึงสามารถเลือกอัลกอริทึม checksum แบบสั้น แล้วอ้างว่าส่ง checksum ที่ยาวกว่ามาได้
    • ตาม Google Security report หากรวม heap buffer overflow เข้ากับการรั่วไหลของข้อมูล จะทำให้ไคลเอนต์ที่มีสิทธิ์อ่านแบบไม่ระบุตัวตนเท่านั้นสามารถรันโค้ดตามอำเภอใจบนเครื่องเซิร์ฟเวอร์ rsync ได้
    • hash_search() สร้าง chunk digest ลงใน char sum2[MAX_DIGEST_LEN] บนสแตกและเปรียบเทียบด้วย memcmp() แต่บัฟเฟอร์ sum2 บนสแตกแบบโลคัลไม่ได้ถูกกำหนดค่าเริ่มต้น จึงอาจมีข้อมูลสแตกค้างอยู่ในไบต์ส่วนที่เหลือ
    • ไคลเอนต์ที่เป็นอันตรายสามารถรั่วข้อมูลได้ 1 ไบต์ต่อการดาวน์โหลดไฟล์หนึ่งครั้ง และข้อมูลที่รั่วอาจรวมถึง pointer ไปยัง heap object, stack cookie, ตัวแปรโลคัล, pointer ไปยังตัวแปรโกลบอล และ return pointer
    • “Some checksum buffer fixes” ทำให้ s->s2length ที่ผู้โจมตีควบคุมได้ไม่สามารถมากกว่าความยาว checksum สำหรับการถ่ายโอนได้ และ “prevent information leak off the stack” กำหนดค่าเริ่มต้นหน่วยความจำ sum2 เป็น 0
    • Go กำหนดค่าเริ่มต้นให้ตัวแปรทั้งหมดเป็น zero value จึงไม่ได้รับผลกระทบจากช่องโหว่นี้ และ gokrazy/rsync ใช้โปรโตคอลเวอร์ชัน 27 ไม่ใช่โปรโตคอลเวอร์ชัน 30 ที่เพิ่มการเลือก checksum นอกเหนือจาก MD4
  • CVE-2024-12087: path traversal ผ่าน symbolic link, ความรุนแรง 7.5

    • ตาม Google Security report เมื่อเปิดการซิงก์ symbolic link ด้วย -l หรือ -a (--archive) เซิร์ฟเวอร์ที่เป็นอันตรายสามารถทำให้ไคลเอนต์เขียนไฟล์ใดก็ได้ที่อยู่นอกไดเรกทอรีเป้าหมาย
    • การโจมตีทำโดยส่งรายการไฟล์หลายชุดในโหมด --inc-recursive โดยในรายการแรกส่ง symlink เป็นไดเรกทอรี และในรายการถัดไปเปลี่ยนชื่อเดิมนั้นให้เป็น symbolic link ที่ชี้ออกนอกไดเรกทอรีเป้าหมาย
    • Go เองไม่สามารถป้องกันช่องโหว่นี้ได้ เพราะสาเหตุคือข้อผิดพลาดเชิงตรรกะที่ไม่ได้ตรวจสอบซ้ำหลังรวมรายการไฟล์หลายชุด
    • การแก้ไข upstream เพิ่มการตรวจสอบที่ขาดหายไป
    • gokrazy/rsync ไม่ได้รับผลกระทบ เพราะไม่ได้ implement โหมด incremental recursion (--inc-recursive)
    • incremental recursion ช่วยให้ประมวลผลแบบ “windowed” โดยไม่ต้องสแกนชุดไฟล์ทั้งหมดก่อนเริ่มการถ่ายโอน แต่มี trade-off ระหว่างความซับซ้อนของการ implement กับการใช้ทรัพยากร
  • CVE-2024-12088: การ bypass --safe-links, ความรุนแรง 7.5

    • ตาม Google Security report --safe-links เป็นฟีเจอร์ที่ตรวจสอบว่า symbolic link ที่ได้รับจากเซิร์ฟเวอร์ชี้อยู่ภายในไดเรกทอรีเป้าหมาย
    • การ bypass เกิดขึ้นเพราะไม่ได้คำนึงว่ามี symbolic link อื่นอยู่ระหว่างทางของพาธเป้าหมาย symbolic link หรือไม่
    • ตัวอย่างเช่น หากมี {DESTINATION}/a -> . และ {DESTINATION}/foo -> a/a/a/a/a/a/../../ แล้ว foo จะชี้ออกนอกไดเรกทอรีเป้าหมายจริง แต่ unsafe_symlink() กลับถือว่า a/ เป็นไดเรกทอรีและตัดสินว่าปลอดภัย
    • การแก้ไข upstream ทำให้ unsafe_symlink() เข้มงวดยิ่งขึ้นโดยไม่อนุญาต ../ ในตำแหน่งที่ไม่ใช่ช่วงต้นของพาธ
    • Go เองไม่สามารถป้องกันฟังก์ชันตรวจสอบที่ผิดพลาดได้ และ gokrazy/rsync ยังไม่ได้ implement ฟีเจอร์ --safe-links จึงไม่เปราะบาง
  • CVE-2024-12086: การรั่วไหลของไฟล์ตามอำเภอใจ, ความรุนแรง 6.8

    • rsync receiver ในโหมดไคลเอนต์ไม่ได้จัดระเบียบชื่อไฟล์ที่ rsync sender ส่งมา และไม่สามารถป้องกันการเปิดไฟล์นอก target tree ได้
    • sender ที่เป็นอันตรายสามารถสั่งให้ receiver เปรียบเทียบ checksum ของไฟล์ใดก็ได้ที่อยู่นอก target tree และสังเกตการตอบสนองของ receiver ต่อ checksum ขนาด 1 ไบต์เพื่อรั่วข้อมูลไฟล์ตามอำเภอใจ
    • ตาม Google Security report เมื่อไคลเอนต์เชื่อมต่อกับเซิร์ฟเวอร์ที่เป็นอันตราย เซิร์ฟเวอร์สามารถรั่วเนื้อหาของไฟล์ใดก็ได้บนเครื่องไคลเอนต์
    • การแก้ไข upstream ตรวจสอบพาธที่ sender ให้มาเพื่อป้องกันการเปิดไฟล์นอก target tree
    • Go มี API ที่ใช้ป้องกันเรื่องนี้ได้ โดยมีการเสนอ os.Root ของ Go เป็นแนวทางป้องกันที่เกี่ยวข้อง
    • gokrazy/rsync ไม่เปราะบาง เพราะ implement โปรโตคอลเวอร์ชัน 27 ไม่ใช่โปรโตคอลเวอร์ชัน 29 ที่เพิ่มฟีเจอร์ fuzzy matching
  • CVE-2024-12747: race condition ของ symbolic link, ความรุนแรง 5.6

    • ตาม Red Hat Security Advisory นี่เป็นข้อบกพร่องที่เกิดจาก race condition ระหว่างการจัดการ symbolic link ของ rsync
    • พฤติกรรมเริ่มต้นของ rsync คือข้าม symbolic link เมื่อพบ แต่หากผู้โจมตีเปลี่ยนไฟล์ปกติให้เป็น symbolic link ในจังหวะที่เหมาะสม ก็สามารถ bypass พฤติกรรมเริ่มต้นและทำให้มีการไล่ตาม symbolic link ได้
  • การแก้ไข upstream เปลี่ยนให้การเรียก open() ของตัวส่ง rsync ใช้ออปชัน O_NOFOLLOW

    • gokrazy/rsync มีช่องโหว่อยู่จนถึงก่อน commit 1b1fbf6 โดยคอมมิตนี้ได้นำมาตรการบรรเทา O_NOFOLLOW แบบเดียวกับ upstream rsync มาใช้
    • Damien Neil เห็นว่า การแก้ไข CVE-2024-12747 ของ gokrazy ยังไม่เพียงพอ และพิจารณาว่า O_NOFOLLOW ป้องกันได้เพียงการตาม symbolic link ขององค์ประกอบเส้นทางตัวสุดท้ายเท่านั้น
    • ตัวอย่างเช่น ใน os.Open("dir/passwd") หากเปลี่ยนองค์ประกอบเส้นทางก่อนหน้าอย่าง dir ให้เป็น symbolic link ที่ชี้ไปยัง /etc ก็ยังสามารถหลบเลี่ยงได้
    • ปัญหานี้ถูกรายงานไปยังช่องทางติดต่อด้านความปลอดภัยของ rsync ในเดือนเมษายน 2025 และนำไปสู่ CVE-2026-29518 ที่เปิดเผยเมื่อ 2026-05-20

ช่องโหว่เดือนพฤษภาคม 2026

  • CVE-2026-29518: race condition ของ symbolic link, ความรุนแรง 7.0

    • ตาม rsync 3.4.3 NEWS entry นี่คือ TOCTOU symbolic link race condition ที่อนุญาตให้ยกระดับสิทธิ์ภายในเครื่องในโหมด daemon โดยไม่ใช้ chroot
    • rsync daemon ที่ตั้งค่า use chroot = no มีความเสี่ยงต่อการแข่งขันแบบ time-of-check/time-of-use บน parent path component
    • ผู้โจมตีภายในเครื่องที่มีสิทธิ์เขียนไปยังโมดูลสามารถสลับ parent directory component ให้เป็น symbolic link ระหว่างการตรวจสอบของ receiver กับ open() ทำให้เกิดการเปิดเผย basis-file ในการอ่าน และการเขียนทับไฟล์นอกโมดูลในการเขียน
    • ค่าเริ่มต้น use chroot = yes ไม่ได้รับผลกระทบ
    • การแก้ไข upstream ใช้ secure_relative_open() ซึ่งคล้ายกับ API os.Root ของ Go
    • gokrazy/rsync เคยมีช่องโหว่จนกระทั่งเปลี่ยน sender และ receiver ไปใช้ API os.Root ที่ต้านทานการ traversal
  • CVE-2026-43618: การรั่วไหลของหน่วยความจำจากระยะไกลจาก integer overflow, ความรุนแรง 8.1

    • ตัวถอดรหัส compressed token ของ rsync receiver สะสมตัวนับ signed 32 บิตโดยไม่ตรวจสอบ overflow ทำให้ sender ที่เป็นอันตรายสามารถรั่วไหลเนื้อหาหน่วยความจำของโปรเซสได้
    • ข้อมูลที่รั่วไหลอาจรวมถึงตัวแปรสภาพแวดล้อม รหัสผ่าน heap และ library pointer ซึ่งทำให้ ASLR อ่อนแอลงและช่วยให้ exploit ต่อเนื่องทำได้ง่ายขึ้น
    • ขอบเขตผลกระทบคือการเชื่อมต่อ daemon แบบ authenticated ที่เปิดใช้การบีบอัด และจะเปิดใช้งานโดยค่าเริ่มต้นเมื่อทั้งสองฝั่งโฆษณาว่ารองรับการบีบอัดในโปรโตคอล 30 ขึ้นไป
    • แนวทางบรรเทาคือปิดการบีบอัดของ daemon ด้วย refuse options = compress ใน rsyncd.conf
    • การแก้ไข upstream เพิ่มการตรวจสอบที่ขาดหายไป
    • gokrazy/rsync ไม่ได้ implement การบีบอัด จึงไม่เปราะบาง และเหตุผลที่แม้รองรับการบีบอัดจะดูเหมือนเรียบง่ายแต่ไม่ใช่เรื่องตรงไปตรงมานั้นสรุปไว้ใน gokrazy/rsync issue #35
  • CVE-2026-43620: การปฏิเสธการให้บริการหลังการอ่านนอกขอบเขต, ความรุนแรง 6.5

    • เกิดจาก guard parent_ndx<0 ที่เพิ่มใน send_files() เมื่อปี 2025 ไม่ได้ถูกนำไปใช้กับบล็อก recv_files() ที่เหมือนกันทางสายตา
    • หาก rsync server ที่เป็นอันตรายส่ง compatibility flag CF_INC_RECURSE พร้อม flist ที่จัดรูปแบบผิด receiver อาจอ่านและ dereference dir_flist->files[-1] จนนำไปสู่ SIGSEGV แบบกำหนดได้แน่นอน
    • ขอบเขตผลกระทบคือ rsync client ทั้งหมดที่ทำ pull ตามปกติจาก URL ที่ผู้โจมตีควบคุม และไม่ต้องมีตัวเลือกพิเศษฝั่งเหยื่อเพราะ inc_recurse เป็นค่าเริ่มต้นของโปรโตคอล 30 ขึ้นไป
    • การใช้ --no-inc-recursive ที่ฝั่ง client เป็นแนวทางบรรเทา และ การแก้ไข upstream ได้เพิ่ม guard parent_ndx<0 ให้กับ recv_files() ด้วย
    • gokrazy/rsync ไม่ได้ implement incremental recursive mode ของ --inc-recursive จึงไม่ได้รับผลกระทบ เช่นเดียวกับ CVE-2024-12087
  • CVE-2026-43619: race condition ของ symbolic link เพิ่มเติม, ความรุนแรง 6.3

    • การแก้ไข race condition ของ symbolic link สำหรับการเรียก open() ของ receiver (CVE-2026-29518) ไม่ได้ครอบคลุมการเรียกระบบแบบอิงพาธอื่น ๆ เช่น chmod, lchown, utimes, rename, unlink, mkdir, symlink, mknod, link, rmdir, lstat
    • ใน rsync daemon ที่ตั้งค่า use chroot = no ผู้โจมตีภายในเครื่องสามารถสลับ parent directory component ให้เป็น symbolic link ระหว่างการตรวจสอบของ receiver กับ system call เพื่อ redirect ออกนอกโมดูลที่ export อยู่ได้
    • ค่าเริ่มต้น use chroot = yes ไม่ได้รับผลกระทบ
    • การแก้ไข upstream จัดการ system call แบบอิงพาธที่ได้รับผลกระทบผ่าน parent dirfd ที่เปิดภายใต้ข้อจำกัดที่ kernel บังคับใช้ เช่น openat2 บน Linux 5.6+, O_RESOLVE_BENEATH บน FreeBSD 13+ และ macOS 15+, หรือการ traversal แบบ O_NOFOLLOW ทีละ component บนสภาพแวดล้อมอื่น
    • gokrazy/rsync ไม่ได้รับผลกระทบเพราะใช้ API os.Root ของ Go อย่างทั่วถึง
  • CVE-2026-43617: การข้าม ACL ที่อิง hostname, ความรุนแรง 4.8

    • ใน rsync daemon ที่มีการตั้งค่า rsyncd.conf แบบ global daemon chroot = /X การทำ reverse DNS lookup ของ client ที่เชื่อมต่อถูกดำเนินการหลังจาก daemon chroot ไปยัง /X แล้ว
    • หากใน /X ไม่มี /etc/resolv.conf, /etc/nsswitch.conf, /etc/hosts และ NSS service module ที่จำเป็นต่อการ resolve ของ glibc การ lookup จะล้มเหลวและ hostname ของฝั่งที่เชื่อมต่อจะถูกตั้งเป็น "UNKNOWN"
    • ทำให้ กฎ deny ที่อิง hostname เช่น hosts deny = *.evil.example ไม่แมตช์ และผู้โจมตีที่ควบคุม PTR record ได้สามารถเชื่อมต่อจาก hostname ที่ผู้ดูแลระบบตั้งใจบล็อกไว้ได้
    • ACL ที่อิง IP ไม่ได้รับผลกระทบ และการตั้งค่า use chroot รายโมดูลไม่เกี่ยวข้องกับช่องโหว่นี้
    • การแก้ไข upstream ย้ายการทำ DNS lookup ไปยังช่วงต้นของโปรโตคอลมากขึ้น
    • gokrazy/rsync ไม่ได้ implement รายการ allow/deny ที่อิง hostname และ implement เฉพาะรายการ allow/deny ที่อิง IP จึงไม่เปราะบาง
  • CVE-2026-45232: การเขียนออกนอกขอบเขตของสแตก, ความรุนแรง 3.1

    • การรองรับพร็อกซี HTTP CONNECT ของ rsync client มีการเขียนออกนอกขอบเขตของสแตกแบบ off-by-one ใน establish_proxy_connection()
    • หากพร็อกซีหรือผู้โจมตีแบบ man-in-the-middle ส่งบรรทัดตอบกลับแรกที่ยาว 1023 ไบต์ขึ้นไปโดยไม่มี '\n' โค้ดถัดไปอาจเขียน '\0' ต่อท้ายบัฟเฟอร์ 1024 ไบต์โดยตรง ทำให้สล็อตสแตกข้างเคียงเสียหายได้
    • AddressSanitizer รายงาน stack-buffer-overflow ที่ socket.c:95 ในเฟรม establish_proxy_connection
    • การแก้ไข upstream ตรวจสอบข้อมูลที่ผู้โจมตีส่งมา
    • gokrazy/rsync ไม่ได้ implement การรองรับพร็อกซี แบบนี้ จึงไม่เปราะบาง

สิ่งที่ Go และ gokrazy/rsync ป้องกันได้จริง

  • การตรวจสอบขอบเขต ของรันไทม์ Go เปลี่ยนปัญหาความปลอดภัยที่ร้ายแรงกว่านั้นให้กลายเป็น panic ซึ่งแม้ panic จะยังมีความเสี่ยงด้านการปฏิเสธการให้บริการ แต่ก็ถือเป็นรูปแบบความล้มเหลวที่ดีกว่า
  • Go กำหนดค่าเริ่มต้นหน่วยความจำเป็น 0 ทำให้การรั่วไหลของข้อมูลแบบ CVE-2024-12085 เป็นไปไม่ได้
  • API os.Root ของ Go ป้องกันช่องโหว่ที่เหลืออยู่ส่วนใหญ่ได้
  • จากช่องโหว่ทั้งหมด 12 รายการ มี เพียง CVE-2026-43617 รายการเดียว ที่จัดเป็นบั๊กตรรกะของแอปพลิเคชันซึ่งการใช้ Go ไม่สามารถป้องกันได้
  • ความต่างสำคัญระหว่าง gokrazy/rsync กับ rsync upstream อย่างเป็นทางการ นอกจากจะเขียนด้วย Go แล้ว ยังอยู่ที่การมี การติดตั้งใช้งานแบบมินิมอล อีกด้วย
  • gokrazy/rsync ไม่ได้ติดตั้งใช้งานฟีเจอร์ที่เป็นปัญหาหลายอย่าง เช่น --inc-recursive, --safe-links, การบีบอัด, พร็อกซี, และ ACL ตามชื่อโฮสต์ จึงหลีกเลี่ยงช่องโหว่หลายรายการได้
  • เช่นเดียวกับ rsync implementation ทั้งหมดที่เข้ากันได้กับ wire protocol, gokrazy/rsync ตั้งเป้าที่โปรโตคอลเวอร์ชัน 27 และโปรโตคอลเวอร์ชันหลังจากนั้นได้เพิ่มความซับซ้อนเข้ามาอย่างมาก
  • สถานะของ gokrazy/rsync แยกตาม CVE ณ เวลาที่เผยแพร่:
    • 2024-12084: มีการติดตั้งใช้งานและเกิด panic
    • 2024-12085: เป็นฟีเจอร์ของโปรโตคอล 30 จึงไม่เปราะบางเพราะไม่ได้ติดตั้งใช้งาน
    • 2024-12086: เป็นฟีเจอร์ของโปรโตคอล 29 จึงไม่เปราะบางเพราะไม่ได้ติดตั้งใช้งาน
    • 2024-12087: ไม่ได้ติดตั้งใช้งาน inc-rec จึงไม่เปราะบาง
    • 2024-12088: ไม่ได้ติดตั้งใช้งาน safe-links จึงไม่เปราะบาง
    • 2024-12747: มีการติดตั้งใช้งานและเคยเปราะบาง
    • 2026-29518: มีการติดตั้งใช้งานและแพตช์แล้ว
    • 2026-43617: ไม่ได้ติดตั้งใช้งานรายการ deny ของโฮสต์ จึงไม่เปราะบาง
    • 2026-43618: ไม่ได้ติดตั้งใช้งานการบีบอัด จึงไม่เปราะบาง
    • 2026-43619: มีการติดตั้งใช้งานและแพตช์แล้ว
    • 2026-43620: ไม่ได้ติดตั้งใช้งาน inc-rec จึงไม่เปราะบาง
    • 2026-45232: ไม่ได้ติดตั้งใช้งานพร็อกซี จึงไม่เปราะบาง
  • ช่องโหว่ที่ทราบทั้งหมดของ gokrazy/rsync ได้รับการแก้ไขแล้ว และสถานะข้างต้นแสดงสถานะ ณ เวลาที่แต่ละ CVE ถูกเปิดเผย
  • ตอนที่มีการเปิดเผยช่องโหว่ในเดือนมกราคม 2025 gokrazy/rsync เกิด panic ใน CVE-2024-12084 และเปราะบางต่อ race condition แบบ TOCTOU ใน CVE-2024-12747
  • ระหว่างการแก้ปัญหา TOCTOU ได้มีการค้นพบ CVE-2026-29518 และได้แก้ไขใน gokrazy/rsync ก่อนที่ CVE ดังกล่าวจะถูกเปิดเผย
  • CVE-2026-43619 ถูกค้นพบช้ากว่านั้น แต่ใน gokrazy/rsync ได้รับการแก้ไขไปแล้วด้วยการแก้แบบเดียวกัน คือ การใช้ os.Root ของ Go อย่างครอบคลุมทั้งหมด

การแยกบทบาทและการตั้งชื่อช่องโหว่

  • รายงานช่องโหว่หลายฉบับใช้คำว่า “server” และ “client” แต่ในการรับส่ง rsync ทั้งฝั่งไคลเอนต์และฝั่งเซิร์ฟเวอร์ของ rsync ต่างก็สามารถทำหน้าที่เป็นผู้ส่ง (sender, อัปโหลดไฟล์) หรือผู้รับ (receiver, ดาวน์โหลดไฟล์) ได้
  • ในโหมด daemon สามารถจำกัดการเข้าถึงไฟล์ระบบไว้ที่เส้นทางโมดูลที่ตั้งค่าไว้ล่วงหน้าได้ แต่ใน command mode ทำเช่นนั้นไม่ได้
  • การจัดวาง 4 แบบและชั้นของบทบาท/โปรโตคอลสามารถดูได้จากไดอะแกรม rsync combinations
  • ชื่อเดิมของช่องโหว่ Arbitrary File Leak (CVE-2024-12086) คือ “Server leaks arbitrary client files” ซึ่งทำให้เข้าใจผิดได้ง่าย
  • คำอธิบายที่แม่นยำกว่าคือ ผู้รับ rsync ทำให้ไฟล์ตามอำเภอใจรั่วไหลไปยัง ผู้ส่งที่เป็นอันตราย
  • ผู้ส่งฝั่งไคลเอนต์ที่เป็นอันตรายสามารถทำให้ rsync ระยะไกลที่ยังไม่แพตช์ในสภาพแวดล้อมอย่าง command mode ผ่าน SSH เปิดไฟล์ที่อยู่นอกทรีเป้าหมายได้ เช่น ฐานข้อมูลรหัสผ่านของระบบ /etc/shadow
  • ในโหมด daemon เซิร์ฟเวอร์จะเปิดใช้การทำ path sanitization เพิ่มเติมเพื่อป้องกันการโจมตีนี้
  • ช่องโหว่ Symlink Path Traversal (CVE-2024-12087) ก็ถูกอธิบายว่าเป็น “malicious server” เช่นกัน แต่ที่ถูกต้องคือ ผู้ส่งที่เป็นอันตราย ซึ่งอาจเป็นได้ทั้งฝั่งไคลเอนต์หรือฝั่งเซิร์ฟเวอร์

เปรียบเทียบกับ openrsync ของ OpenBSD

  • โครงการ OpenBSD เป็นที่รู้จักในด้านการมุ่งเน้นความปลอดภัย และ openrsync ตรวจสอบความยาวของเช็กซัมและรองรับเพียง MD4 เดียวในด้านขนาด/อัลกอริทึมของเช็กซัม จึงไม่ได้รับผลกระทบจาก Heap Buffer Overflow (CVE-2024-12084) และ Stack Info Leak (CVE-2024-12085)
  • openrsync เช่นเดียวกับ gokrazy/rsync ไม่ได้ติดตั้งใช้งานฟีเจอร์ที่เกี่ยวข้อง จึงไม่ได้รับผลกระทบจาก CVE-2024-12086, CVE-2024-12087 และ CVE-2024-12088
  • แม้สมมติว่าจะเปราะบาง เมื่อรันบน OpenBSD ก็อาจมี defense in depth ที่จำกัดการเข้าถึงไฟล์ระบบด้วย OpenBSD unveil(2) และ pledge(2) ช่วยป้องกันการโจมตีที่สำเร็จได้
  • openrsync ใช้ O_NOFOLLOW มาตั้งแต่เริ่มติดตั้งใช้งานการรองรับ symbolic link จึงไม่ได้รับผลกระทบจาก CVE-2024-12747
  • อย่างไรก็ตาม O_NOFOLLOW ไม่ใช่การแก้ไขที่เพียงพอสำหรับปัญหานี้ ดังนั้น openrsync จึงได้รับผลกระทบจาก CVE-2026-29518
  • ชุดช่องโหว่เดือนพฤษภาคม 2026 ก็มีลักษณะคล้ายกันในแง่ที่ส่วนใหญ่เกี่ยวข้องกับฟีเจอร์ที่ไม่ได้ติดตั้งใช้งาน
  • openrsync ไม่ได้รับผลกระทบจากช่องโหว่ที่มีการรายงานส่วนใหญ่ เพราะมีการติดตั้งใช้งานการตรวจสอบอย่างรอบคอบ จำกัดพื้นผิวการโจมตี และใช้ defense in depth

การป้องกันหลายชั้นและ os.Root

  • Linux mount namespace

    • ภายในไม่กี่สัปดาห์หลังเริ่มโปรเจกต์ gokrazy/rsync ก็มีการเพิ่มความสามารถในการลดสิทธิ์และใช้ mount/pid namespace บน Linux เพื่อจำกัดการเข้าถึงอ็อบเจ็กต์ในระบบไฟล์
    • แนวทางนี้ใช้บรรเทา การโจมตีแบบ path traversal ได้ดี แต่ต้องใช้สิทธิ์ จึงต้องรันด้วย root หรือรันภายใน Linux user namespace หากดิสทริบิวชัน/ระบบเปิดใช้ไว้
    • mount namespace เหมาะกับการตั้งค่าเซิร์ฟเวอร์ แต่บ่อยครั้งใช้ไม่ได้กับการโอนย้ายแบบโต้ตอบครั้งเดียวที่รันด้วยบัญชีผู้ใช้ทั่วไป
  • การ hardening ด้วย systemd

    • คอมมิตเดียวกันที่เพิ่มการรองรับ Linux mount/pid namespace ยังมีไฟล์ service ของ systemd ที่จำกัดการเข้าถึงระบบไฟล์ไว้ที่โฮมไดเร็กทอรี และใน README ก็แนะนำให้จำกัดการเข้าถึงระบบไฟล์เพิ่มตามกรณีใช้งาน
    • หากตั้งค่าข้อจำกัดระบบไฟล์เหล่านี้อย่างถูกต้อง ก็จะช่วยบรรเทาช่องโหว่ File Leak (CVE-2024-12086) และ Path Traversal (CVE-2024-12087)
    • Symlink Race Condition (CVE-2024-12747) อาศัยการยกระดับสิทธิ์ผ่านโปรเซส rsync แต่ด้วยฟีเจอร์ DynamicUser ทำให้โปรเซสมีสิทธิ์น้อยกว่าผู้ใช้รายอื่น
    • เช่นเดียวกับ mount namespace คือเหมาะกับการตั้งค่าเซิร์ฟเวอร์ แต่ยุ่งยากเกินไปสำหรับการตั้งค่าเพื่อใช้งานแบบโต้ตอบครั้งเดียว
  • Linux Landlock

    • Porting OpenBSD pledge() to Linux (2022) ทำให้นึกขึ้นได้ว่า Linux ก็มี Landlock API ซึ่งเป็นการควบคุมการเข้าถึงแบบไม่ต้องใช้สิทธิ์และแยกตามโปรเซส คล้ายกับ unveil(2) ของ OpenBSD
    • แนวคิดหลักคือ เมื่อโปรแกรมรู้แล้วว่าจะทำงานกับไดเร็กทอรีใด ก็สามารถเรียกอย่าง unveil("/home/michael/backups", "rw"); เพื่อไม่ให้เข้าถึงตำแหน่งอื่นในระบบไฟล์นอกเหนือจากพาธนั้นได้อีก
    • ในเดือนมีนาคม 2025 มีการเพิ่มการรองรับ Landlockเพื่อจำกัดการเข้าถึงระบบไฟล์
    • เมื่อตั้งค่าเสร็จแล้ว แม้จะรัน gokrazy/rsync แบบไม่ใช้สิทธิ์พิเศษ ก็สามารถจำกัดการโอนย้าย rsync ให้ฝั่งต้นทางอ่านอย่างเดียว และให้ไดเร็กทอรีปลายทางอ่าน-เขียนได้
    • ข้อเสียคือ Landlock ทำงานในระดับโปรเซส
    • นโยบาย Landlock ต้องรวมไฟล์ที่โปรแกรมจำเป็นต้องใช้ด้วย เช่น gokrazy/rsync ต้องอ่าน /etc/passwd เพื่อค้นหา user ID ดังนั้นถ้าผู้โจมตีเล็ง /etc/passwd อยู่ Landlock ก็ช่วยไม่ได้
  • os.Root ของ Go

    • ในเดือนกุมภาพันธ์ 2025 Go 1.24 ได้เพิ่ม API os.Root ที่ทนทานต่อ path traversal โดยมีการสรุปภูมิหลังที่เกี่ยวข้องไว้ใน The Go Blog: Traversal-resistant file APIs ของ Damien Neil
    • เมื่อเทียบกับ Landlock แล้ว os.Root ให้การควบคุมที่ละเอียดกว่าตามแต่ละการทำงานของระบบไฟล์
    • Go 1.25 ที่ออกในเดือนสิงหาคม 2025 ได้เพิ่มเมธอดให้ os.Root มากขึ้น ทำให้เป็นตัวเลือกที่สะดวกสำหรับการใช้งานระบบไฟล์ส่วนใหญ่
    • การใช้งานระบบไฟล์ทั้งหมดของ gokrazy/rsync ถูกเปลี่ยนไปใช้ os.Root แล้ว ซึ่งเข้ากับโมเดลที่ผู้ใช้เป็นคนกำหนดไดเร็กทอรีรับเข้า/ส่งออก แต่ชื่อไฟล์ที่รับมาจากเครือข่ายไม่น่าเชื่อถือ
    • แม้แต่ system call ที่ดูเหมือนจะสร้างผ่าน API โดยตรงไม่ได้อย่าง mknod(2) ก็ยังใช้งานได้อย่างปลอดภัย
    • Damien Neil อธิบายว่าสามารถใช้ os.Root.OpenFile เพื่อเปิดไดเร็กทอรีแม่ของปลายทาง จากนั้นใช้ File.Fd เพื่อรับ file descriptor ของไดเร็กทอรีนั้น แล้วสร้างไฟล์ด้วย golang.org/x/sys/unix#Mknodat
    • ตัวอย่างการใช้งานจริงอยู่ที่ internal/receiver/generatormknod_linux.go, line 15-29
    • Linux มี mknodat(2) แต่ ณ Linux 7.0 ยังไม่มี bindat ที่สอดคล้องกับ bind(2)
    • Lennart Poettering เสนอว่า หากไม่มี bindat ก็สามารถใช้ทริก bind ไปที่ /proc/self/<fd>/foobar เพื่อข้ามการตีความพาธได้
    • หากหลัง /proc/self/<fd> ซึ่งรู้ว่าปลอดภัยอยู่แล้ว ระบุไม่ใช่พาธแต่เป็น basename หรือองค์ประกอบสุดท้ายของพาธ ก็จะข้ามการตีความพาธได้ และโค้ดที่เกี่ยวข้องอยู่ที่ line 49-56
    • ด้วยเคล็ดลับสองข้อนี้ gokrazy/rsync v0.3.1 ขึ้นไปจึงใช้ os.Root ได้อย่างสมบูรณ์ และการเข้าถึงระบบไฟล์ทั้งหมดก็มี ความปลอดภัยต่อ path traversal

บทเรียนสำคัญ

  • ช่องโหว่ทั้งหมด ยกเว้นช่องโหว่ TOCTOU (CVE-2024-12747, CVE-2026-29518, CVE-2026-43619) มีต้นตอมาจากการไม่มีการตรวจสอบอินพุต หรือการตรวจสอบอินพุตที่ผิดพลาด
  • ในสามกรณี ไม่มีการตรวจสอบตั้งแต่แรก ส่วน CVE-2024-12088 เป็นกรณีที่หัวข้อเรื่องการตีความพาธของระบบไฟล์มีความซับซ้อน ทำให้การตรวจสอบเดิมครอบคลุมทุกกรณีขอบไม่ครบ
  • การแก้ไขเชิงโครงสร้างที่มีคุณค่ามากที่สุดคือการมีการตรวจสอบที่เปิดใช้งานตลอดเวลา เช่น boundary check และมี API ที่ปลอดภัยโดยปริยาย อย่าง os.Root ของ Go
  • ช่องโหว่บางส่วนเกิดจากวิวัฒนาการของโปรโตคอล rsync โดยมีการเพิ่มฟีเจอร์ใหม่เข้าไปในโค้ดที่เดิมมีการตรวจสอบเพียงพอ แต่ไม่ได้อัปเดตการตรวจสอบให้ถูกต้องตามฟีเจอร์ใหม่
  • การเจรจาเลือกอัลกอริทึม checksum และ incremental recursion ถูกเพิ่มเข้ามาในโปรโตคอลเวอร์ชัน 30 และการตรวจสอบเดิมก็ไม่ได้ถูกอัปเดตให้สอดคล้องกับวิธีจัดการของฟีเจอร์ใหม่
  • gokrazy/rsync และ openrsync ไม่ได้รับผลกระทบจากช่องโหว่ 8 รายการ จากทั้งหมด 12 รายการ เพียงเพราะไม่ได้ implement ฟีเจอร์ที่มีช่องโหว่
  • ฟีเจอร์เหล่านั้นถูกเพิ่มเข้า rsync เพราะ ณ เวลาหนึ่งมันมีคุณค่าสำหรับใครบางคน และไม่ได้หมายความว่าควรหยุดพัฒนาซอฟต์แวร์
  • ทางเลือกที่เหมาะสมที่สุดคือใช้ implementation ที่มีความซับซ้อนเหมาะสมและได้สัดส่วนกับความซับซ้อนของกรณีใช้งาน โดยเลือก implementation ที่เรียบง่ายสำหรับกรณีใช้งานที่เรียบง่าย และเลือก implementation แบบครบฟีเจอร์เมื่อจำเป็นเท่านั้น

1 ความคิดเห็น

 
GN⁺ 2 시간 전
ความคิดเห็นจาก Lobste.rs
  • เป็นบทความที่ยอดเยี่ยมมาก ดีขนาดที่ทุกภาษาควรมี API อย่าง os.Root อยู่ใน standard library
    หลังจากเจอ ช่องโหว่ path traversal ใน SecureDrop มาหลายครั้ง เราก็ใช้เวอร์ชันที่ลดความซับซ้อนลงมาก และตอนนี้ถ้าใช้ API ที่ถูกต้อง ช่องโหว่ทั้งหมวดหนึ่งก็หายไปได้เลย
    • เห็นด้วย ดูเหมือนว่าพระเอกของบล็อกโพสต์นี้จะไม่ใช่ Go แต่เป็น os.Root มากกว่า