Go rsync ที่ปลอดภัยต่อหน่วยความจำแบบมินิมอลของฉันหลีกเลี่ยงช่องโหว่ได้อย่างไร
(michael.stapelberg.ch)- 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เป็น512Go runtime จะทำให้เกิดpanic: runtime error: slice bounds out of range [:512] with length 16 - การทำให้เซิร์ฟเวอร์ล่มทั้งตัวไม่ใช่โหมดความล้มเหลวที่พึงประสงค์ จึงได้ เพิ่มการตรวจสอบขอบเขตที่ขาดหายไปเพื่อเปลี่ยน panic ให้เป็น error
- rsync เปรียบเทียบความยาว checksum ที่ได้รับจากเครือข่ายกับ
-
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 กับการใช้ทรัพยากร
- ตาม Google Security report เมื่อเปิดการซิงก์ symbolic link ด้วย
-
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จึงไม่เปราะบาง
- ตาม Google Security report
-
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
- gokrazy/rsync มีช่องโหว่อยู่จนถึงก่อน commit
ช่องโหว่เดือนพฤษภาคม 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()ซึ่งคล้ายกับ APIos.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 อาจอ่านและ dereferencedir_flist->files[-1]จนนำไปสู่SIGSEGVแบบกำหนดได้แน่นอน - ขอบเขตผลกระทบคือ rsync client ทั้งหมดที่ทำ pull ตามปกติจาก URL ที่ผู้โจมตีควบคุม และไม่ต้องมีตัวเลือกพิเศษฝั่งเหยื่อเพราะ
inc_recurseเป็นค่าเริ่มต้นของโปรโตคอล 30 ขึ้นไป - การใช้
--no-inc-recursiveที่ฝั่ง client เป็นแนวทางบรรเทา และ การแก้ไข upstream ได้เพิ่ม guardparent_ndx<0ให้กับrecv_files()ด้วย - gokrazy/rsync ไม่ได้ implement incremental recursive mode ของ
--inc-recursiveจึงไม่ได้รับผลกระทบ เช่นเดียวกับ CVE-2024-12087
- เกิดจาก guard
-
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 อย่างทั่วถึง
- การแก้ไข race condition ของ symbolic link สำหรับการเรียก
-
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 จึงไม่เปราะบาง
- ใน rsync daemon ที่มีการตั้งค่า rsyncd.conf แบบ global
-
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 การรองรับพร็อกซี แบบนี้ จึงไม่เปราะบาง
- การรองรับพร็อกซี HTTP
สิ่งที่ 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: มีการติดตั้งใช้งานและเกิด panic2024-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 ก็ช่วยไม่ได้
- Porting OpenBSD pledge() to Linux (2022) ทำให้นึกขึ้นได้ว่า Linux ก็มี Landlock API ซึ่งเป็นการควบคุมการเข้าถึงแบบไม่ต้องใช้สิทธิ์และแยกตามโปรเซส คล้ายกับ
-
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/rsyncv0.3.1 ขึ้นไปจึงใช้os.Rootได้อย่างสมบูรณ์ และการเข้าถึงระบบไฟล์ทั้งหมดก็มี ความปลอดภัยต่อ path traversal
- ในเดือนกุมภาพันธ์ 2025 Go 1.24 ได้เพิ่ม API
บทเรียนสำคัญ
- ช่องโหว่ทั้งหมด ยกเว้นช่องโหว่ 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 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
os.Rootอยู่ใน standard libraryหลังจากเจอ ช่องโหว่ path traversal ใน SecureDrop มาหลายครั้ง เราก็ใช้เวอร์ชันที่ลดความซับซ้อนลงมาก และตอนนี้ถ้าใช้ API ที่ถูกต้อง ช่องโหว่ทั้งหมวดหนึ่งก็หายไปได้เลย
os.Rootมากกว่า