פלטפורמת למידה המאפשרת לתלמידים לכתוב שאילתות MongoDB ויתרה במודול ה-vm של Node.js, וכעת מנתחת כל שאילתה לעץ תחביר מופשט (AST) לפני הביצוע. השינוי מסיר sandbox שעלולה להיפרץ, ובכך מגן על ה-backend מפני קוד שרירותי שמחרוזת מעוותת עלולה להזריק.
מדוע סביבת הארגון המקורית נכשלה
המימוש הראשון עטף ידית (handle) של מסד נתונים חי בקריאה ל-vm.runInContext וניסה להגביל את המשתמשים באמצעות ביטוי רגולרי (regular expression). הוא אפשר רק שמות של מתודות כגון find או aggregate; כל מזהה אחר אמור היה להיות מסונן החוצה.
שתי פגמים הפכו את הגישה הזו ללא מאובטחת:
- ניתן לעקוף סינון באמצעות Regex. JavaScript מאפשרת לקוד להשתמש בסימון סוגריים מרובעים (
obj["constructor"]) כדי להגיע לכל מאפיין (property). תוקף יכול לשלוף את ה-Functionconstructor, לבנות פונקציה חדשה ולהריץ כל קוד שיבחר. ה-regex לעולם לא רואה את הגישה למאפיין שבבסיס, מכיוון שניתן לכתוב מחדש את המקור באינספור דרכים. vmאינו גבול אבטחה. התיעוד של Node מציין ש-vmמבודד את האובייקט ה-global אך לא את התהליך (process) כולו. על ידי הזרקת חיבור מסד נתונים חי לתוך ה-sandbox, לקוד שבתוך ההקשר (context) נותרה היכולת לקרוא לכל מתודה בחיבור זה, כולל כאלו שכותבות או מוחקות נתונים. ה-sandbox לא מנע מהקוד להשפיע על תהליך המארח (host process).
הפתרון המבוסס על AST
הצוות החליף את הרצת הקוד בניתוח סטטי. מחרוזות השאילתה מוזנות כעת למנתח (parser) ה-acorn, שמייצר AST — ייצוג עצי של המבנה התחבירי של הקוד. ה-AST נבדק צומת אחר צומת מול רשימת מותרות (whitelist) קשיחה:
- ערכים קבועים (Literals), מערכים ואובייקטים מותרים רק כאשר הם מופיעים כערכים פשוטים.
- קריאות למתודות מוגבלות לקבוצה מוגדרת מראש (
find,sort,limitוכו'). כל קריאה אחרת נדחית. - גישה למאפיין מחושב (למשל,
obj[expr]) או כל סוג צומת אחר שלא צוין במפורש גוררים כישלון מיידי.
מכיוון שהמנתח עובד על העץ ולא על הטקסט הגולמי, לא ניתן להוליך אותו שולל באמצעות כתיבים חלופיים או טריקים של סימון סוגריים מרובעים. שרשרת constructor שהייתה חומקת מה-regex מופיעה כצומת לא מזוהה ונדחית לפני הרצת הקוד.
המשמעות מבחינת אבטחה
העיצוב החדש עוקב אחר פילוסופיית "דחייה כברירת מחדל" (deny by default):
- הגדירו מה מותר, לא מה אסור. רשימת מותרות טקסטואלית אינה יכולה להיות מקיפה; ל-AST יש סט סופי של סוגי צמתים, מה שהופך בדיקה מקיפה לישימה.
- לעולם אל תחשפו משאבים חיים בתוך sandbox. העברת ידית של מסד נתונים לתוך הקשר מבודד מעניקה לקוד ב-sandbox קו ישיר ל-backend. גישת המנתח לעולם אינה מעבירה אובייקט חי לקוד המשתמש; היא רק מחלצת את כוונת השאילתה.
- אמתו את המבנה, ואז בצעו בבטחה. ברגע שה-AST עובר ולידציה, הפלטפורמה מתרגמת את הקריאות המותרות למתודות של ה-MongoDB driver באמצעות נתיב קוד מהימן משלה.
מה כדאי לעקוב אחריו בהמשך
- בצעו ביקורת (Audit) על כל שימוש ב-
eval,new Function, אוvmבבסיס הקוד שלכם. אפילו רשימת מותרות יכולה להיות מושבתת על ידי הטבע הדינמי של JavaScript. - אמצו ניתוח AST עבור קוד שנוצר על ידי משתמשים בכל מקום שניתן. ספריות כמו
acorn,esprima, אוbabel-parserהופכות את התהליך לפשוט. - הגבילו את החשיפה של אובייקטים חיים. אם יש צורך שחיבור מסד נתונים, ידית קובץ או סוקט רשת יהיו נגישים, עטפו אותם ב-proxy שחושף רק את המתודות המינימליות שאתם מתכוונים לאפשר.
- אוטומציה של בדיקת מקרי קצה. צרו שאילתות המשתמשות בסימון סוגריים מרובעים, מפתחות מחושבים או מניפולציה של prototype כדי לוודא שהמנתח שלכם דוחה אותם.
שורה תחתונה
הסתמכות על vm.runInContext כסביבת ארגון (sandbox) מעניקה תחושת אבטחה כוזבת; מסנני regex אינם יכולים לכסות את התחביר הגמיש של JavaScript, וסביבת הארגון אינה מבודדת משאבי תהליך. ניתוח קלט משתמש ל-AST וקביעת רשימת מותרות רק לצמתים שאתם מבינים מספקת מחסום מוחשי וניתן לתחזוקה שעוצר קוד זדוני לפני שהוא רץ. אם הפלטפורמה שלכם מאפשרת למשתמשים לכתוב קוד, החליפו הרצת סגנון eval בניתוח סטטי כבר היום.
