แพลตฟอร์มการเรียนรู้ที่ให้นักเรียนเขียนคิวรี MongoDB ได้ ตัดการใช้งานโมดูล vm ของ Node.js ออก และเปลี่ยนมาใช้การวิเคราะห์ทุกคิวรีให้อยู่ในรูปแบบ abstract syntax tree (AST) ก่อนการประมวลผล การเปลี่ยนแปลงนี้ช่วยกำจัด sandbox ที่อาจถูกเจาะได้ ซึ่งเป็นการปกป้อง backend จากโค้ดอันตรายที่อาจถูกฉีดเข้ามาผ่านสตริงที่สร้างขึ้นเพื่อโจมตี

ทำไม sandbox แบบเดิมถึงล้มเหลว

การปรับใช้ในครั้งแรกใช้วิธีการห่อหุ้ม database handle ที่ใช้งานจริงไว้ในคำสั่ง vm.runInContext และพยายามจำกัดสิทธิ์ผู้ใช้ด้วย regular expression โดยอนุญาตให้ใช้เฉพาะชื่อเมธอด เช่น find หรือ aggregate เท่านั้น ส่วน identifier อื่นๆ ทั้งหมดจะถูกกรองออก

ข้อบกพร่องสองประการทำให้แนวทางนั้นไม่ปลอดภัย:

  • การกรองด้วย Regex สามารถถูกหลบเลี่ยงได้ JavaScript อนุญาตให้โค้ดใช้ bracket notation (obj["constructor"]) เพื่อเข้าถึง property ใดก็ได้ ผู้โจมตีสามารถดึง Function constructor ออกมา สร้างฟังก์ชันใหม่ และรันโค้ดใดก็ได้ตามต้องการ โดยที่ regex จะไม่เห็นการเข้าถึง property พื้นฐานเลย เพราะ source code สามารถเขียนใหม่ได้ในรูปแบบที่นับไม่ถ้วน
  • vm ไม่ใช่ขอบเขตด้านความปลอดภัย (security boundary) เอกสารของ Node ระบุว่า vm แยกเฉพาะ global object ออกมา แต่ไม่ได้แยกทั้ง process การฉีดการเชื่อมต่อฐานข้อมูลที่ใช้งานจริงเข้าไปใน sandbox ทำให้โค้ดภายใน context นั้นยังคงมีความสามารถในการเรียกใช้เมธอดใดๆ บนการเชื่อมต่อดังกล่าวได้ รวมถึงเมธอดที่เขียนหรือลบข้อมูลด้วย sandbox จึงไม่สามารถหยุดโค้ดจากการส่งผลกระทบต่อ host process ได้

แนวทางแก้ไขโดยใช้ AST

ทีมงานได้เปลี่ยนจากการประมวลผลโค้ดมาเป็นการวิเคราะห์แบบ static analysis โดยสตริงของคิวรีจะถูกส่งไปยัง parser อย่าง acorn เพื่อสร้าง AST ซึ่งเป็นการแสดงโครงสร้างไวยากรณ์ของโค้ดในรูปแบบต้นไม้ (tree representation) จากนั้น AST จะถูกตรวจสอบทีละโหนด (node-by-node) เทียบกับ whitelist ที่เข้มงวด:

  • Literals, arrays และ objects จะได้รับอนุญาตเฉพาะเมื่อปรากฏเป็นค่าธรรมดา (plain values) เท่านั้น
  • Method calls จะถูกจำกัดไว้เฉพาะชุดที่กำหนดไว้ล่วงหน้า (find, sort, limit และอื่นๆ) หากมีการเรียกใช้เมธอดอื่นจะถูกปฏิเสธทันที
  • การเข้าถึง computed property (เช่น obj[expr]) หรือโหนดประเภทอื่นที่ไม่ได้ระบุไว้ในรายการ จะทำให้เกิดความล้มเหลวทันที

เนื่องจาก parser ทำงานบนโครงสร้างต้นไม้ ไม่ใช่ข้อความดิบ (raw text) มันจึงไม่สามารถถูกหลอกด้วยการสะกดคำที่ต่างออกไปหรือเทคนิค bracket notation ได้ โครงสร้าง constructor chain ที่อาจหลุดรอดการตรวจสอบของ regex จะปรากฏเป็นโหนดที่ไม่รู้จักและถูกปฏิเสธก่อนที่โค้ดจะเริ่มทำงาน

สิ่งนี้หมายถึงอะไรในแง่ของความปลอดภัย

การออกแบบใหม่นี้ใช้ปรัชญาแบบ “deny by default” (ปฏิเสธไว้ก่อน):

  • กำหนดสิ่งที่อนุญาต ไม่ใช่สิ่งที่ห้าม การทำ allow-listing ด้วยข้อความไม่สามารถครอบคลุมได้ทั้งหมด แต่ AST มีประเภทของโหนดที่จำกัด ทำให้การตรวจสอบอย่างละเอียดสามารถทำได้จริง
  • อย่าเปิดเผยทรัพยากรที่ใช้งานจริงภายใน sandbox การส่ง database handle เข้าไปใน context ที่แยกออกมา จะทำให้โค้ดใน sandbox มีช่องทางเชื่อมต่อตรงไปยัง backend แนวทางแบบ parser จะไม่ส่ง object ที่ใช้งานจริงให้กับโค้ดของผู้ใช้ แต่จะดึงเพียงเจตนา (intent) ของคิวรีออกมาเท่านั้น
  • ตรวจสอบโครงสร้าง แล้วจึงประมวลผลอย่างปลอดภัย เมื่อ AST ผ่านการตรวจสอบแล้ว แพลตฟอร์มจะแปลงการเรียกใช้ที่ได้รับอนุญาตให้เป็นเมธอดของ MongoDB driver จริงๆ โดยใช้เส้นทางการทำงาน (code path) ที่เชื่อถือได้ของตัวเอง

สิ่งที่ควรเฝ้าระวังต่อไป

  • ตรวจสอบการใช้ eval, new Function หรือ vm ใน codebase ของคุณ แม้แต่ whitelist ก็อาจถูกทำลายได้ด้วยธรรมชาติที่ยืดหยุ่นของ JavaScript
  • นำการทำ AST parsing มาใช้กับโค้ดที่สร้างโดยผู้ใช้ ในทุกที่ที่เป็นไปได้ ไลบรารีอย่าง acorn, esprima หรือ babel-parser จะช่วยให้การแปลงข้อมูลนี้ทำได้ง่ายขึ้น
  • จำกัดการเข้าถึง live objects หากจำเป็นต้องเข้าถึงการเชื่อมต่อฐานข้อมูล, file handle หรือ network socket ให้ห่อหุ้มมันไว้ใน proxy ที่เปิดเผยเฉพาะเมธอดขั้นต่ำที่คุณตั้งใจจะอนุญาตเท่านั้น
  • ทดสอบ edge cases โดยอัตโนมัติ สร้างคิวรีที่ใช้ bracket notation, computed keys หรือการจัดการ prototype เพื่อตรวจสอบว่า parser ของคุณปฏิเสธสิ่งเหล่านี้จริงหรือไม่

บทสรุป

การพึ่งพา vm.runInContext ในฐานะ sandbox ให้ความรู้สึกปลอดภัยที่ผิดพลาด (false sense of security) เพราะ regex filter ไม่สามารถครอบคลุมไวยากรณ์ที่ยืดหยุ่นของ JavaScript ได้ และ sandbox ก็ไม่ได้แยกทรัพยากรของ process ออกจากกัน การแปลง input ของผู้ใช้ให้เป็น AST และทำ whitelist เฉพาะโหนดที่คุณเข้าใจ จะช่วยสร้างปราการที่จับต้องได้และดูแลรักษาง่าย ซึ่งจะหยุดโค้ดอันตรายได้ก่อนที่มันจะเริ่มทำงาน หากแพลตฟอร์มของคุณอนุญาตให้ผู้ใช้เขียนโค้ด จงเปลี่ยนจากการประมวลผลแบบ eval-style มาเป็นการวิเคราะห์แบบ static analysis ตั้งแต่วันนี้