אז ידידי נעם רותם מצא חולשת אבטחה משמעותית ביותר בכביש 6. באג שחושף את כל החשבוניות של כל הנוסעים. מה זה אומר? זה אומר שכל מאגר הנתונים של הנהגים בכביש 6. השעות שבהם הם נסעו, המכוניות שבהם הם נסעו וכל פרט מידע אחר – היה זמין לכל אחד בכל זמן נתון. נעם פירט באופן מקיף על הפירצה הזו בטוויטר ומומלץ לקרוא את השרשור שלו בטוויטר על העניין הזה (הוא נכנס גם לפרטים טכניים – פשוט תתחילו מהציוץ הראשון ) :
לפני יומיים קיבלתי חשבונית מכביש שש, נכנסתי לצפות בה, יאדה יאדה יאדה, כל החשבוניות של כל המשתמשים היו פתוחות, כמו גם פירוט הנסיעות בכביש, מספרי הרכבים, ועוד שלל תופינים. https://t.co/3akEqcZX1A
סייבר!— Noam R (@noamr) August 29, 2018
הכתבה עצמה מאוד מעניינת וראוי לציין את העיתונאי אמיתי זיו שכתב באופן פשוט וידידותי על הפירצה הזו.
גם אני וגם נעם מסכימים שמציאת חולשות וסגירתן היא רק אספקט אחד בכל הנוגע לאבטחה. מה שחשוב הוא חינוך ולימוד מתכנתים. זה מה שאני עושה בעשור האחרון. אז התנדבתי לכתוב פוסט שאולי ילמד על נקודות הכשל האלו. מי יודע? אולי נצליח להציל נפש אחת. ניסיתי להיות פשוט ככל האפשר והשתמשתי בדוגמאות משתי שפות פופולריות, לא צריך להיות מתכנת כדי להבין אותן.
בחולשת האבטחה של כביש 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, אז אתם צריכים לוודא במה הפריימוורק שלכם תומך או לממש משהו משל עצמכם (שזה רע, תזרמו עם הפריימוורק). אין מצב שיש בקשה למשאב שלא עוברת דרך בדיקה מהסוג שתיארנו.
11 תגובות
הי,
אין לינט שיכול לעלות על הדברים האלה?
קצת מפתיע אותי השימוש בהשוואה לפונקציות בוליאניות בדוגמאות שלעיל:
is_numeric($_GET['id']) === true
Number.isInteger(req.query.id) === false
It's called code readability.
השאלה היא האם היה אפשר להשתמש במספר רץ כרגיל אבל עדיין לעשות וולידציה שהמשתמש מבקש חשבונית שבהכרח רשומה על שמו, זה היה מספיק טוב?
כן, אבל למה לתת לתוקף מידע בחינם? ואם יש לך api חשוף במקום אחר? בוא נלך על גם וגם.
זה נקרא Security through obscurity וכפי שהפתגם מרמז זה לא נחשב באמת בטיחותי יותר , אלא רק "פחות מזמין" על משקל "פירצה קוראת לגנב".
אם יש לך המון קריאות למשאבים , לייצר שדה נוסף, שאילתות ועוד עבור כל אחד מהן רק בשביל הסתרה זה קצת בזבזני, הדבר הנכון בסופו של דבר הוא לבדוק הרשאה גישה למשאב.
כפי ש-owasp עצמם מציינים (פה: https://www.owasp.org/index.php/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet) , זה נכון. אבל אני ממש את זה תמיד כי לך תדע וב-node\MongoDB זה לא יקר בכלל. אבל כל אחד והטעם שלו. מה שחשוב הוא להיות מודע.
כל הכבוד על החשיפה.
אנשים לא מודעים לזה . כשמדובר בקובץ שהוא ציבורי כמו תמונה/חשבונית וכו'
אין להשתמש במספר סידורי שאפשר לחזות אותו מראש. ולהריץ סריקה ולשאוב את המידע . יש להשתמש בפונקצית גיבוב כמו sha256
ואז לתת את הלינק הזה, וככה לא יהיה ניתן לסרוק את האתר
הid של מונגו ממש לא מאבטח
https://stackoverflow.com/questions/4587523/mongodb-is-it-safe-to-use-documents-id-in-public
לזכרונני מונגו לא באמת נותן I'd מאובטח….
אפשר להנדס את זה לאחור, אבל זה יותר קשה משמעותית ובדרך כלל מרתיע את התוקף הפוטנציאלי.