פעם חשבתי שכתיבת שכבת האימות (authentication layer) שלי היא תג של גאווה. אם אתה מבין ב-JWTs ויודע לבצע hash לסיסמה, כמה קשה זה יכול להיות? חיברתי backend ב-Node, הנפקתי טוקנים, וקראתי לזה גמור. הקוד עבד. הטסטים עברו. ואז התחלתי לקרוא על איך התקפות אמיתיות מתרחשות בפועל, והקרקע נסחפה מתחתיי. כל פרק על injection, enumeration ודליפות side-channel שלח אותי בחזרה לעורך שלי עם תחושה רעה בבטן. מערכת האימות שלי לא סתם הייתה עם פרצות; היו לה ארבע דלתות פתוחות לרווחה שהתקנתי בעצמי. הנה מה שמצאתי, ומה בדיוק שיניתי.
SQL Injection באמצעות שרשור מחרוזות
הבאג הראשון היה הטריק הכי ישן בספר. לקחתי קלט ממשתמשים והכנסתי אותו ישירות לתוך מחרוזות SQL. בנתיב ה-login שלי, לקחתי את האימייל מגוף הבקשה (request body) ושרשרתי אותו לתוך שאילתה כמו SELECT * FROM users WHERE email = '${email}'. זה הרגיש חסר נזק כי שליטתי ב-frontend. זו הנחה מסוכנת. תוקף לא צריך את ה-frontend שלך. גוף POST אחד שעוצב במיוחד יכול להפוך את בדיקת ההתחברות הזו לפריצת נתונים או למחיקה של מסד הנתונים. שליחת payload כמו ' OR '1'='1 או גרוע מכך, שאילתה מרובת פקודות (stacked query) שמוחקת טבלאות, ואם המחרוזת מבוצעת כפי שהיא, הנתונים שלך אובדים. לא נתתי למסד הנתונים שום דרך להבחין בין הקוד שלי לבין הנתונים של התוקף.
התיקון לא היה אימות קלט (input validation) נוסף או escaping של מחרוזות ידני. התיקון האמיתי היה שאילתות פרמטריות (parameterized queries). עברתי ל-node-postgres והתחלתי להשתמש ב-placeholders כמו $1. השאילתה הופכת לתבנית: SELECT * FROM users WHERE email = $1. הדרייבר שולח את ה-SQL ואת הערכים בערוצים נפרדים. מסד הנתונים מתייחס לקלט אך ורק כנתונים, לא משנה אילו תווים הוא מכיל. השינוי הזה סוגר את כל קטגוריית התקפות ה-injection. זה פשוט יותר לקריאה, קל יותר לתחזוקה, וזה מסיר ממך את הצורך להיות קוסם regex בכל פעם שאתה כותב פסוקת WHERE.
אינדוקציה (Enumeration) של כתובות אימייל באמצעות הודעות שגיאה
הטעות השנייה שלי נראתה כמו UX טוב. כשמשתמש הקליד אימייל שגוי, החזרתי User not found. כשהאימייל היה נכון אך הסיסמה שגויה, החזרתי Incorrect password. זה הרגיש מועיל. זה שימש גם ככלי לאיסוף מודיעין (reconnaissance) עבור תוקפים. סקריפטים של enumeration יכולים להציף את ה-login endpoint שלך באלפי כתובות אימייל. אם גוף התגובה או קוד הסטטוס משתנים בהתאם לשאלה אם החשבון קיים, הסקריפט יכול לבנות רשימה מאומתת של המשתמשים שלך. הרשימה הזו הופכת לבסיס ל-credential stuffing, פישינג ממוקד וניסיונות brute-force נוספים.
הייתי חייב להשלים עם העובדה שחווית משתמש ידידותית צריכה לפעמים להפסיד לביטחון. שיניתי כל נתיב התחברות שנכשל כך שיחזיר בדיוק את אותה מחרוזת: Invalid credentials. בלי רמזים. בלי לוגיקת הסתעפות (branching logic) בתגובת השגיאה. בין אם האימייל חסר, הסיסמה שגויה או שהחשבון נעול, הטקסט נשאר זהה. זה תקף גם לתהליכי הרשמה ואיפוס סיסמה; אל תחשפו אם כתובת כבר קיימת במערכת שלכם. הודעה גנרית אחת מסירה דליפת מידע שתוקפים מסתמכים עליה.
התקפות תזמון (Timing Attacks) בהשוואת סיסמאות
הבאג השלישי היה בלתי נראה. אני
