นักบัญชีที่มีสิทธิ์อ่านอย่างเดียว (read-only) สามารถยกเลิกใบแจ้งหนี้และลบประวัติการชำระเงินในเครื่องมือทำบัญชีแบบโอเพนซอร์สอย่าง Akaunting ได้ ซึ่งทำให้ธุรกิจขนาดเล็กเสี่ยงต่อการสูญเสียข้อมูลโดยไม่รู้ตัว ช่องโหว่นี้ได้รับการแก้ไขแล้วในเวอร์ชัน 3.2.0 แต่ความผิดพลาดที่เกิดจากการผูกการตรวจสอบสิทธิ์เข้ากับรายการชื่อเมธอดที่กำหนดไว้ตายตัว (hard-coded) ยังคงเป็นภัยคุกคามต่อระบบใดก็ตามที่พึ่งพาการควบคุมการเข้าถึงตามบทบาท (role-based access control)
ช่องโหว่นี้หลุดรอดไปได้อย่างไร
API ของ Akaunting ตรวจสอบสิทธิ์ของผู้ใช้โดยการอ้างอิงจากรายการอนุญาต (allowlist) ของชื่อเมธอด รายการดังกล่าวครอบคลุมการดำเนินการ CRUD พื้นฐาน ได้แก่ create, read, update, delete แต่กลับพลาด endpoint ที่ใช้เปลี่ยนสถานะหลายรายการ:
markSentmarkCancelledmarkReceived
เนื่องจาก handler เหล่านี้ไม่ได้อยู่ในรายการ ทำให้ framework ไม่ได้เรียกใช้ขั้นตอนการตรวจสอบสิทธิ์เมื่อมีการทำงานเกิดขึ้น ผู้ใช้ที่มีบทบาทแบบ read-only สามารถส่งคำขอ GET แบบง่ายๆ ไปยัง endpoint markCancelled และระบบจะถือว่าการเปลี่ยนแปลงสถานะนั้นเป็นการกระทำที่ถูกต้องตามกฎ
การยกเลิกใบแจ้งหนี้ทำได้มากกว่าแค่การทำเครื่องหมายว่าเอกสารนั้นเป็นโมฆะ แต่มันยังลบประวัติการชำระเงินใดๆ ที่เชื่อมโยงกับใบแจ้งหนี้นั้นด้วย ผลลัพธ์คือ ผู้ใช้ที่ไม่มีสิทธิ์แก้ไขสามารถลบเส้นทางการเงินของธุรกรรมทิ้งไปได้ทั้งหมด
ผลการทดสอบแสดงให้เห็นอะไร
ช่องโหว่นี้ปรากฏใน Docker image อย่างเป็นทางการของ Akaunting:
- การส่งคำขอ PUT มาตรฐานเพื่ออัปเดตใบแจ้งหนี้ส่งคืนค่า 403 Forbidden ซึ่งยืนยันว่าเส้นทางการอัปเดตปกติได้รับการป้องกันไว้
- การส่งคำขอ GET ไปยัง endpoint สำหรับการยกเลิกกลับสำเร็จโดยไม่มีข้อผิดพลาดด้านการอนุญาต (authorization error) ซึ่งเผยให้เห็นช่องโหว่ดังกล่าว
ทำไมเรื่องนี้ถึงสำคัญ
งบการเงินสามารถถูกเปลี่ยนแปลงได้โดยไม่มีร่องรอยการตรวจสอบ (audit trail) ที่ชัดเจน ทำให้การตรวจพบการทุจริตทำได้ยากขึ้น และความผิดพลาดที่ไม่ได้ตั้งใจก็แก้ไขได้ยากขึ้นด้วย
การแก้ไข
เวอร์ชัน 3.2.0 ได้ขยายแผนผังการอนุญาต (permission map) ให้ครอบคลุมการดำเนินการเปลี่ยนสถานะที่เคยตกหล่นไป ตั้งแต่เวอร์ชันนั้นเป็นต้นมา คำขอใดๆ ที่เปลี่ยนสถานะของเอกสาร ไม่ว่าจะถูกทำเครื่องหมายว่าส่งแล้ว (sent), ยกเลิก (cancelled) หรือได้รับแล้ว (received) จะต้องผ่านการตรวจสอบบทบาทแบบเดียวกับการอัปเดตมาตรฐาน สิ่งนี้ช่วยกู้คืนความคาดหวังที่ว่าบทบาทแบบ read-only จะไม่สามารถแก้ไขข้อมูลได้จริงๆ
บทเรียนสำหรับนักพัฒนา
- อย่าใช้ชื่อเมธอดเป็นตัวตัดสินความปลอดภัย การเพิ่ม endpoint ใหม่ไม่ได้หมายความว่าจะได้รับการป้องกันโดยอัตโนมัติ ควรตรวจสอบ side effects ของแต่ละ public method เสมอ
- Allowlist จะสมบูรณ์เท่ากับรายการที่มีอยู่เท่านั้น รายการของคำกริยาที่ "อนุญาต" แบบคงที่ (static list) อาจเปิดช่องว่างให้เกิดความผิดพลาดจากการมองข้ามได้
- แยกเจตนาออกจาก HTTP verb โดยปกติ GET มีไว้สำหรับอ่านข้อมูลเท่านั้น แต่ในกรณีนี้มันกลับเปลี่ยนสถานะข้อมูล ดังนั้นควรจำกัดการเปลี่ยนแปลงข้อมูล (mutations) ไว้ที่ POST, PUT, DELETE, PATCH เท่านั้น
- ตรวจสอบความครอบคลุมของการอนุญาตแบบอัตโนมัติ เครื่องมือ Static analysis สามารถแจ้งเตือนเมธอดใน controller ที่ขาดการเรียกใช้การตรวจสอบสิทธิ์ (authorization call) เพื่อช่วยตรวจพบช่องโหว่ก่อนที่จะปล่อยซอฟต์แวร์
- ทดสอบด้วยบัญชีที่มีสิทธิ์น้อยที่สุด (least-privilege accounts) การทดสอบผ่าน Docker ใช้ผู้ใช้ที่มีสิทธิ์แบบ read-only การจำลองสถานการณ์เช่นนี้ใน CI pipelines จะช่วยให้พบปัญหาที่คล้ายกันได้ตั้งแต่เนิ่นๆ
สิ่งที่ควรติดตามต่อไป
ชุมชนของ Akaunting ได้ปล่อยเวอร์ชันที่แก้ไขแล้วออกมาเรียบร้อยแล้ว ผู้ดูแลระบบควรตรวจสอบเวอร์ชันของระบบที่ใช้งานอยู่และดำเนินการอัปเดตโดยทันที
สำหรับนักพัฒนาที่กำลังสร้างระบบที่อิงตามบทบาท (role-based system) บทเรียนที่ได้รับนั้นชัดเจน: โมเดลการอนุญาตที่ต้องอาศัยการจดจำทุกการกระทำที่เป็นไปได้นั้นมีความเปราะบางโดยธรรมชาติ ควรประกาศให้ชัดเจนว่าการดำเนินการใดบ้างที่เปลี่ยนสถานะข้อมูล บังคับใช้การตรวจสอบในระดับ framework และตรวจสอบ codebase อย่างสม่ำเสมอ เมื่อทำเช่นนี้เท่านั้น ป้ายกำกับ "read-only" จึงจะได้รับความไว้วางใจว่าสามารถรักษาบันทึกทางการเงินให้คงเดิมได้
