עוד תקלה מביכה באתר מרכזי ותשתיתי – כך נמנעים ממנה

כשלים במערכת התשלומים של כביש 6 גרמה לחשיפת כל המידע המלא של כל מי שנסע בכביש 6.
חשבונית של כביש 6

אז ידידי נעם רותם מצא חולשת אבטחה משמעותית ביותר בכביש 6. באג שחושף את כל החשבוניות של כל הנוסעים. מה זה אומר? זה אומר שכל מאגר הנתונים של הנהגים בכביש 6. השעות שבהם הם נסעו, המכוניות שבהם הם נסעו וכל פרט מידע אחר – היה זמין לכל אחד בכל זמן נתון. נעם פירט באופן מקיף על הפירצה הזו בטוויטר ומומלץ לקרוא את השרשור שלו בטוויטר על העניין הזה (הוא נכנס גם לפרטים טכניים – פשוט תתחילו מהציוץ הראשון ) :

הכתבה עצמה מאוד מעניינת וראוי לציין את העיתונאי אמיתי זיו שכתב באופן פשוט וידידותי על הפירצה הזו.

גם אני וגם נעם מסכימים שמציאת חולשות וסגירתן היא רק אספקט אחד בכל הנוגע לאבטחה. מה שחשוב הוא חינוך ולימוד מתכנתים. זה מה שאני עושה בעשור האחרון. אז התנדבתי לכתוב פוסט שאולי ילמד על נקודות הכשל האלו. מי יודע? אולי נצליח להציל נפש אחת. ניסיתי להיות פשוט ככל האפשר והשתמשתי בדוגמאות משתי שפות פופולריות, לא צריך להיות מתכנת כדי להבין אותן.

בחולשת האבטחה של כביש 6 היו שתי נקודות כשל מרכזיות. הראשונה היא גישה למשאבים למי שאינו מורשה והשניה היא שימוש ב-ID רץ כמזהה משאב (אני משתמש במילה משאב ל resource שיכול להיות הרבה דברים: רשומה במערכת, קובץ וכו׳).

מה זה ID רץ? בואו ונניח שיש לי ממשק שמחזיר תשובה על כל ID. למשל משהו בסגנון הזה ב-PHP.

<?php 
if ( $_GET['id'] && is_numeric($_GET['id'])  === true ) {
  $id = $GET['id'];
  $stmt = $pdo->prepare("SELECT * FROM sometable WHERE id=?");
  $stmt->execute([$id]); 
  $result = $stmt->fetch();
}

או למשל משהו כזה ב-node:


app.get('/', function(req, res) {
  if( Number.isInteger(req.query.id) === false ) {
    res.status(500);
  } else {
     const id= req.query.id;
     connection.query('SELECT * FROM sometable WHERE id=?', [id], (err, result) => {
    // Do something.
   }
  }
});

קודם כל אפשר לראות שעשיתי ולידציה למשתנה שמגיע מה-GET. שנית אפשר לראות שלא השתמשתי בהשמה של משתנים ישירות ב-Query. אבל זה בקטנה. כשיש כזה דבר לצורך העניין, בדרך כלל אנו נראה ב-URL את ה-id. וזה בדיוק מה שנעם וכל מי שצפה בחשבונית של כביש 6 ראה:

kvish6.co.il/?id=1234567890

וזה כבר רע, כי זה נותן לתוקף פרט מידע חשוב שעדיף לא לתת לו. הרי בסופו של דבר, id יכול להיות כל דבר. לא רק מספר. אבל ברגע שאנחנו מכניסים מספר, אנחנו נותנים לתוקף מידע על מבנה הנתונים שלנו. אם המספר שלנו הוא 1234567890 אנחנו למשל מגלים לו שיש 1234567889 חשבוניות קודמות. שהוא יכול למצוא חשבונית שהמספר שלה הוא 1234567889 או 1200000000 (למשל) והוא יכול לעשות את זה דרך ה-URL.

מה הפתרון? פשוט ביותר. בשלב ה-CREATE של הרשומה? פשוט תיצרו מספר זהות אחר ואל תסתמכו על המספר הרץ (auto increment). איזה מספר זהות אחר? יצרו אותו באמצעות הפונקציה hash ב-PHP או במודול crypto של node.js (הוא בא כמודול טבעי ולא צריך להתקינו) הפונקציה תקבל את התאריך שבו האובייקט נוצר למשל. ברגע שיש לנו מספר סתום לחלוטין – כמות המידע שאנו נותנים קטנה יותר.

אם אנו משתמשים ב-MongoDB ודומיו, אנו מקבלים את ה-id הזה במתנה כמובן.

לפעמים אנחנו חייבים מספר זהות רץ, חשבוניות זה מקרה קלאסי – אבל חייבים להקפיד על מפתח נוסף שהוא זה שמשמש אותנו לגישה אל המשאב שאנחנו צריכים. המספר הרץ? רק כמידע נוסף ולא כמזהה עיקרי.

אבל במקרה של כביש 6 לא עשו את זה, וכך נעם, אחרי שהבין מידע קריטי על המערכת, ניגש להתקפה. מה ההתקפה שלו היתה? ובכן, הוא ניסה לגשת לחשבונית שלא שלו והצליח. מה נקודת הכשל? למרות שנעשתה אותנטיקציה, לא היה פיקוח על המשאבים שאפשר לקרוא להם עם ה-role המתאים. כלומר לכל משתמש מוגדר role ונעשתה בדיקה אך ורק בנוגע לrole ולא לשום דבר אחר. כך למשל, מי שרוצה לגשת אל המשאב, נבדקים לגביו שני דברים: הראשון הוא אם הוא ביצע אותנטיקצה והשני אם יש לו תפקיד מספק. במקרה של נעם הוא באמת עשה אותנטיקציה וכנראה ה-role שלו היה של regular user. וזה כמובן רע מאוד.

לכל משאב במערכת צריך להיות מוגדר בדיוק מי יכול לראות אותו. אם נניח שיש לנו שני רמות משתמשים: מנהלים (שיכולים לראות כל דבר במערכת) ומשתמשים רגילים, אז הבדיקה תהיה:

1. האם יש לו role של מנהל? במידה וכן, הוא יכול לגשת למשאב. במידה ולא, עבור ל-2.
2. האם ה-id שלו מופיע ברשומה של המשאב? במידה וכן, הוא יכול לגשת למשאב. במידה ולא, עבור ל-3.
3. הגש למשתמש דף 403 ודווח למערכת.

זו כמובן גישה נאיבית מאוד. כי בלא מעט מערכות יש לנו כמה דרגות של שימוש. למשל משתמש רגיל, נציג שירות ומנהל. יש דברים שנציג שירות יכול לראות למשל אבל לא את הכל. במקרה הזה, אנו צריכים שתהיה לנו בצד הרשומה לא רק את ה-ID של המשתמש שיצר אותה אלא גם role של כאלו שיכולים לראות את הרשומה. במקרה הזה הבדיקה תהיה:

1. האם יש למשתמש role של מנהל? במידה וכן, הוא יכול לגשת למשאב. במידה ולא, עבור ל-2.
2. האם יש למשתמש role שתואם ל-roles המופיעים ברשומת המשאב? במידה וכן, הוא יכול לגשת למשאב. במידה ולא, עבור ל-3.
3. האם ה-id שלו מופיע ברשומה של המשאב? במידה וכן, הוא יכול לגשת למשאב. במידה ולא, עבור ל-4.
4. הגש למשתמש דף 403 ודווח למערכת.

כלומר שבטבלה של המשאבים (במקרה של כביש 6, החשבוניות) יהיו עוד שני טורים. בטור הראשון יהיה את היוצר שאליו משויך המשאב. במקרה הזה ה-id של המשתמש. בטור השני, את התפקידים שיכולים לגשת למשאב. במקרה הזה רק המנהלים (כמובן) ואנשי השירות שזקוקים לגשת לחשבונית כאשר לקוח מתקשר לקבל מידע על החשבוניות שלו. (גם זו גישה קצת נאיבית אבל תזרמו איתי).

הבדיקה הזו צריכה להיעשות בכל גישה למשאב. במקרה של node.js זה ממש קל. עושים את זה באמצעות middleware חביב. אפשר לשים אותו באופן ידני, למשל משהו בסגנון הזה:


function isAuthenticated(req, res, next) {
  if (req.user.authenticated)
      return next();
  res.status(403).redirect('/403');
}

app.get('/my-invoice:id', isAuthenticated, (req, res) => {

});

או אפילו להגדיר אותו בכל route. אם אתם משתמשים ב-PHP, אז אתם צריכים לוודא במה הפריימוורק שלכם תומך או לממש משהו משל עצמכם (שזה רע, תזרמו עם הפריימוורק). אין מצב שיש בקשה למשאב שלא עוברת דרך בדיקה מהסוג שתיארנו.

פוסטים נוספים שכדאי לקרוא

גלילה לראש העמוד