פירוש בחינת מומחה אבטחת מידע רשום, אביב 2024 (רייווה 6), שאלת PM 1 — JWT alg=none, הרשאת API והקלה זמנית ב-WAF
· Go Komura · מומחה אבטחת מידע רשום, מומחה אבטחה רשום, API, אבטחת API, JWT, אימות, הרשאה, WAF, Log4Shell, אבטחת מידע, פגיעות, IPA
“אנחנו מאמתים את חתימת ה-JWT, לכן אפשר לסמוך על מזהה המשתמש.”
הקביעה הזאת נכונה רק במחציתה.
שאלה 1 במושב ה-PM (אחר הצהריים) של בחינת מומחה אבטחת מידע רשום, אביב 2024 (רייווה 6), בנויה סביב API שנקרא מאפליקציית סמארטפון.1 לאחר אימות מוצלח מונפק JWT, וה-JWT הזה מצורף לקריאות מול ה-API שמביא ומעדכן מידע משתמש. במבט ראשון זה מערך שגרתי לגמרי.
אולם האבחון מעלה את ארבע הבעיות הבאות.
- שינוי ה-
algבכותרת ה-JWT ל-noneמאפשר ל-JWT בלי חתימה לעבור. - שמירה על JWT תקף אבל שינוי
midלמזהה משתמש אחר מאפשרים לקרוא או לעדכן מידע של מישהו אחר. - הוספת
status=paidשאינו מתועד הופכת משתמש בשכבת חינם למשתמש משלם. - קוד האימות בן ארבע הספרות שנשלח בדוא״ל ניתן לניחוש כוחני בלי הגבלת ניסיונות.
ארבעתן נראות כמו “פגיעויות בסביבת האימות”, אבל הסיבות אינן זהות. מה שנשבר הם גבולות נפרדים: שלמות האסימון, הרשאה ברמת האובייקט, הרשאה ברמת המאפיין, והגבלת קצב ניסיונות.
המחצית השנייה של השאלה מוסיפה עוד נושא. פגיעות קריטית מתפרסמת בספריית קוד פתוח נפוצה, שמאפשרת לתוקף להריץ קוד מרחוק באמצעות ניצול לרעה של JNDI Lookup. אין עדיין תיקון ואין כלל WAF מוכן. בינתיים השאלה שואלת איך לאשר את ההשפעה, היכן על ה-WAF להביט, ולמה מצב ה-WAF הראשוני צריך להיות “זיהוי” ולא “חסימה”.
המאמר נשען על תשובות המודל הרשמיות2 ועל פירוט הציונים3 כבסיס, ועובר לא רק על התשובה לכל שאלה אלא על למה זו התשובה, ועד כמה ביתר קפדנות כדאי לתכנן את זה בפועל.
flowchart TB
accTitle: סקירת השאלה
accDescr: מראה את גבול האמון שנשבר בכל שלב - קוד אימות, JWT, הרשאת API ופגיעות בספרייה
user["אפליקציית המשתמש"]
auth["קוד אימות בן 4 ספרות"]
jwt["הנפקת JWT"]
api["API משתמש"]
log["פלט יומן"]
vuln["ספרייה פגיעה"]
ext["הרצת קוד מרחוק"]
user --> auth
auth -->|אין הגבלת ניסיונות| jwt
jwt -->|מאפשר alg=none| api
api -->|סומך על mid| db1["קריאה/עדכון נתוני משתמש אחר"]
api -->|status=paid| db2["שינוי סטטוס חיוב"]
api --> log
log --> vuln
vuln -->|JNDI/LDAP/HTTP| ext
איור 1: סקירת השאלה. גבול אמון אחר נשבר בכל שלב.
1. השורה התחתונה קודם
- התכונה של API מסוג RESTful שלא מחזיק מצב שיחה נקראת statelessness. זה לא אומר שהשרת אינו מחזיק כלל מסד נתונים או מצב משתמש
- לקוד אימות בן ארבע ספרות יש 10,000 ערכים אפשריים. ב-10 ניסיונות לשנייה תוקף מצליח אחרי 5,000 ניסיונות בממוצע, כלומר 500 שניות — קצר מתקופת התוקף של 10 דקות, ולכן תפוגה לבדה אינה עוצרת אותו
- ההגנה המינימלית מול
alg=noneהיא לוודא שה-algבכותרת ה-JWT אינוNONE. בפועל, עם זאת, צריך לקבע את קבוצת האלגוריתמים המותרים בצד השרת - גם עם JWT תקף, אסור לסמוך על ה-
midשל הבקשה. יש להתאים את מזהה המשתמש שבתוך ה-JWT מולmid, או, בבטחה רבה יותר, לא לקבלmidמהלקוח כלל ולקבוע את היעד מתוך ה-JWT - הוספת
status=paidהיא בעיית Mass Assignment, שבה מאפיינים מחוץ למפרט נקשרים ישר לאובייקט הפנימי. השתמשו ב-DTO של העדכון כרשימת היתר, ואל תתנו למשתמש לשנות סטטוס חיוב - תשובת המודל להגנה מפני ניחוש כוחני היא לוגיקה שנועלת את החשבון ברגע שמספר הכשלונות הרצופים חורג מסף. בפועל מוסיפים גם השהיות מדורגות ובקרות לפי מקור
- כדי לאשר את השפעת פגיעות קריטית שפורסמה זה עתה, במקום להוציא פקודה הרסנית, רושמים גישות ל-
index.htmlשל שרת הבדיקה כדי לאשר שהרצת קוד מרחוק באמת מגיעה אליו - כי מחרוזת ההתקפה נישאת בכותרת HTTP, יעד הבדיקה של ה-WAF הוא
Header. כביטוי רגולרי שמטפל בהחלפת רישיות, משתמשים במשהו כמו\W[jJ][nN][dD][iI]\W - היתרון בהתחלת ה-WAF במצב “זיהוי” הוא שזה מונע מתעבורת עסק לגיטימית להיחסם בגלל התראת שווא. כשמגיעה התראה, בודקים אם זו התקפה אמיתית, ואז עוברים למצב חסימה אחרי שכוונו את הכלל
- ה-WAF הוא רק אמצעי זמני; תיקון השורש הוא עדכון הספרייה שנפגעה לגרסה מתוקנת
2. איך התרחיש ממופה לשאלות
התרחיש מתרחש בחברה G, שמשגרת שירות בריאות חדש. משתמשים מזינים נתונים כמו ארוחות ומשקל גוף דרך אפליקציית סמארטפון ומקבלים הערכות סיכון בריאותי וייעוץ תזונתי. המערכת בנויה בענן, ומשלבת שער API, עיבוד מונחה אירועים ומסד נתונים מנוהל.
השאלה מפשטת שמות מוצרים ושירותים ספציפיים. גם המאמר אינו משחזר את הדיאגרמות או הטקסט של IPA, אלא מנסח מחדש רק את המבנה הדרוש להבנת השאלות.
| שאלה | נושא | פרק במאמר זה |
|---|---|---|
| שאלה 1 | אופי API מסוג RESTful | פרק 4 |
| שאלה 2(1) | זמן ניחוש כוחני של הקוד בן 4 הספרות | פרק 5 |
| שאלה 2(2) | JWT alg=none |
פרק 6 |
| שאלה 2(3) | גישה למשתמש אחר דרך mid |
פרק 7 |
| שאלה 2(4) | הפגם שמקבל status מחוץ למפרט |
פרק 8 |
| שאלה 2(5) | הגנות מפני ניחוש כוחני | פרק 9 |
| שאלה 3(1) | אישור בטוח שקיימת פגיעות | פרק 11 |
| שאלה 3(2)(3) | היכן ה-WAF מביט, והביטוי הרגולרי | פרק 12 |
| שאלה 3(4) | יתרון מצב הזיהוי ואופן ההפעלה | פרק 13 |
פירוט הציונים מציין ששיעור התשובות הנכונות הכללי היה בסביבות הממוצע. עם זאת הוא גם מצביע על כך ששיעור התשובות הנכונות היה נמוך במקצת בהגנה מפני זיוף JWT בשאלה 2(2) ובמנגנון הנדרש בשרת האימות בשאלה 3(1). אף אחת מהן אינה ניתנת למענה מאוצר מילים בלבד. צריך לעקוב איזה ערך שינה התוקף, לאיזה תהליך הוא זרם, והיכן בסוף סמכו עליו.
3. זו אינה רק “בעיית אימות” אחת
פריסת השאלה כולה לפי גבול אמון נותנת את הבא.
[מזהה משתמש / סיסמה]
|
v
[בדיקת קוד בן 4 ספרות] ---- אין הגבלת ניסיונות ----> ניחוש כוחני
|
v
[הנפקת JWT]
|
v
[ספריית JWT] ------- מאפשרת alg=none ------> זיוף מזהה משתמש
|
v
[API משתמש]
| |
| +-- מעביר status בשלמות ----> פגם בהרשאה ברמת המאפיין
|
+-- סומך על mid -----------------------> פגם בהרשאה ברמת האובייקט
[רישום קלט חיצוני ביומן]
|
v
[ספרייה פגיעה] ---- JNDI/LDAP/HTTP ------> הרצת קוד מרחוק
ההבחנה החשובה ביותר כאן היא הבאה.
| בדיקה | השאלה שהיא שואלת | דוגמה שנשברה בתרחיש זה |
|---|---|---|
| אימות | מי אתה | ניחוש כוחני של הקוד בן 4 הספרות |
| אימות אסימון | האם מידע הזהות הזה זויף | alg=none |
| הרשאה ברמת האובייקט | האם למשתמש הזה מותר לגשת לנתוני המשתמש הזה | החלפת mid |
| הרשאה ברמת המאפיין | האם מותר לשנות את השדה הזה | status=paid |
| גבול מקלט לביצוע | האם קלט חיצוני מתפרש כפקודה | JNDI Lookup |
הצלחה בבדיקה אחת לעולם אינה סיבה לדלג על הבאה. משתמש עם JWT תקף אינו בהכרח מורשה לקרוא נתונים של מישהו אחר. משתמש שמותר לו לעדכן את הנתונים שלו אינו בהכרח מורשה לשנות גם את סטטוס החיוב.
ברגע שאפשר להפריד את השלבים האלה, התשובה לכל שאלה מפסיקה להיות משהו שמשננים.
flowchart LR
accTitle: ההבדל בין אימות להרשאה
accDescr: אימות מאשר את הסובייקט, הרשאה מאשרת מה מותר לסובייקט הזה לעשות
auth["אימות<br/>מי אתה"]
authz["הרשאה<br/>מה מותר לך לעשות"]
auth --> authz
איור 2: ההבדל בין אימות להרשאה. האימות קודם; ההרשאה היא בדיקה נפרדת.
4. שאלה 1 — מה המשמעות של “סטטלס”
שאלה 1 שואלת על אחד מעקרונות התכנון של API מסוג RESTful: התכונה של אי-ניהול שיחה.
התשובה היא סטטלס.
סטטלס פירושו שהשרת אינו צריך לזכור את מצב השיחה של הבקשה הקודמת, כי כל בקשה לבדה נושאת את כל מה שנחוץ לעיבודה. בשאלה הזאת אפליקציית הסמארטפון מצמידה JWT לכותרת Authorization בכל בקשה. השרת מאמת את ה-JWT ומזהה ממנו את המשתמש של אותה בקשה.
קריאה שגויה נפוצה היא לקחת “סטטלס” כ”השרת אינו מחזיק מצב כלל”. במציאות הוא בדרך כלל מחזיק את המצבים הבאים.
- מסד הנתונים ששומר מידע משתמש ונתוני בריאות
- סטטוס חיוב
- ערך קוד האימות, תפוגתו וספירת הכשלונות
- מפתח החתימה של ה-JWT
- מידע ביטול, בתכנונים שמשתמשים ברשימת ביטול
- יומנים ורשומות ביקורת
מה שהוא אינו מחזיק הוא מצב שיחה בצד השרת שקיים רק כדי להמשיך שיחה, כתנאי מוקדם שכל קריאת API תלויה בו.
להיות סטטלס גם אינו משפר אוטומטית את האבטחה. שליחת ה-JWT בכל בקשה אכן מקלה על הרחבה אופקית, אבל אם אימות ה-JWT שגוי, השגיאה מתפשטת באופן אחיד גם לכל צומת. תכונה ארכיטקטונית ונכונות אבטחה הם שני דברים שונים.
5. שאלה 2(1) — קוד בן ארבע ספרות נפרץ בממוצע ב-500 שניות
ה-API לאימות שולח מספר בן ארבע ספרות בדוא״ל ברגע שמזהה המשתמש והסיסמה תואמים. אחר כך הוא מנפיק JWT ברגע שמזהה המשתמש והקוד בן ארבע הספרות תואמים. הקוד תקף 10 דקות מרגע היצירה.
באבחון היו אפשריים 10 ניסיונות לשנייה. השאלה שואלת כמה שניות נדרשות, בממוצע, כדי לפרוץ.
החישוב הוא “חצי ממרחב המועמדים”
מספר בן ארבע ספרות, כולל אפס מוביל, כולל את 10,000 האפשרויות הבאות.
0000, 0001, 0002, ... , 9999
אם התשובה הנכונה נבחרת באקראי אחיד, תוקף שמנסה מועמדים לפי הסדר בלי חזרות מגיע לנכון, בממוצע, אחרי חצי ממרחב המועמדים.
מספר ניסיונות ממוצע = 10,000 / 2 = 5,000
זמן ממוצע = 5,000 / 10 ניסיונות לשנייה = 500 שניות
לכן הריק b הוא 500.
המקרה הגרוע ביותר נמשך עד 1,000 שניות, אבל השאלה שואלת על הממוצע. ותקופת התוקף של הקוד היא 600 שניות — ארוכה מזמן הפריצה הממוצע של 500 שניות. לכן נשפט ש”סביר שייפרץ”.
flowchart LR
accTitle: תחושת קנה מידה לקוד אימות בן 4 ספרות
accDescr: ניסיון 10,000 מועמדים ב-10 לשנייה נותן בממוצע 5,000 ניסיונות ו-500 שניות, פחות מתקופת תוקף של 600 שניות
A["10,000 מועמדים"] -->|ממוצע 10,000 / 2 = 5,000 ניסיונות| B["זמן פריצה ממוצע 500 שניות"]
C["תקופת תוקף 600 שניות"] -->|500 שניות פחות מ-600 שניות| D["ניתן לפרוץ בתוך תקופת התוקף"]
איור 9: תחושת קנה המידה לקוד האימות בן ארבע הספרות. ניסיון, בממוצע, של חצי ממרחב המועמדים פורץ אותו בתוך תקופת התוקף.
קיצור זמן התפוגה בלבד מפסיד אם מרחב המועמדים קטן
חוזק קוד האימות אינו נקבע לא ממספר הספרות לבדו ולא מתקופת התוקף לבדה.
מספר הניסיונות האפשריים במהלך תקופת התוקף
= ניסיונות לשנייה x תקופת התוקף
= 10 x 600
= 6,000 ניסיונות
ניסיון ערכים בלי חזרות לפי הסדר מאפשר לתוקף לבדוק 60% מתוך 10,000 האפשרויות בתוך תקופת התוקף. קביעת זמן תפוגה אינה מספיקה לבדה אם מספר הניסיונות אינו מוגבל.
NIST SP 800-63B הנוכחי דורש לפחות שש ספרות לסודות קצרי טווח באימות מחוץ לערוץ, ומחייב הגבלת קצב ניסיונות בכל פעם שלסוד יש פחות מ-64 סיביות של אנטרופיה. הוא גם קורא לא להשתמש בדוא״ל לאימות מחוץ לערוץ.4 תשובת הבחינה עובדת בתוך המפרט הנתון של קוד בן ארבע ספרות שנשלח בדוא״ל, אבל לתכנון חדש בפועל כדאי לבחון מחדש את ההנחה עצמה.
6. שאלה 2(2) — alg=none היא בעיית “לתת לתוקף לבחור את שיטת האימות”
ה-JWT בשאלה מורכב משלושה חלקים: כותרת, מטען וחתימה.
base64url(header).base64url(payload).base64url(signature)
הכותרת רשמה RS256 כאלגוריתם ששימש לחתימה. המטען מכיל את מזהה המשתמש, זמן ההנפקה ותפוגת התוקף.
המאבחן שינה את שני הדברים הבאים.
- שינוי ה-
algבכותרת מ-RS256ל-NONE. - שינוי מזהה המשתמש במטען למשתמש אחר.
שליחת ה-JWT הזה הצליחה באימות ואיפשרה למאבחן להתחזות למישהו אחר.
flowchart LR
accTitle: זרימת התקפת JWT alg=none
accDescr: שינוי alg ל-none ב-JWT תקף וכתיבה מחדש של מזהה המשתמש מאפשרים לבקשה לעבור
A["JWT תקף<br/>alg=RS256<br/>user=user01"] -->|שינוי alg בכותרת ל-none| B["JWT מזויף<br/>alg=none<br/>user=user02"]
B -->|מדלג על אימות חתימה| C["השרת מקבל אותו<br/>כ-user02"]
איור 3: זרימת התקפת JWT עם alg=none. התוקף בוחר את אלגוריתם האימות.
none אינו שגיאת הקלדה
RFC 7519 מגדיר “Unsecured JWT” — JWT בלי חתימה ובלי הצפנה, ש-alg שלו הוא none.5 לכן הערך none אינו משהו שפשוט אינו קיים במפרט.
הבעיה היא שAPI שאמור לקבל רק JWT חתומים קיבל את ה-none שהתוקף ציין.
בכתיבה מושגית, התהליך הפגיע נראה כך.
1. קרא את כותרת ה-JWT.
2. הבט ב-alg הכתוב בכותרת, ובחר את שיטת האימות.
3. אם alg הוא none, אל תאמת את החתימה.
4. סמוך על מזהה המשתמש במטען.
עצם חוזק האבטחה נבחר מקלט שהתוקף שולט בו.
תשובת הבחינה
השאלה שואלת, ב-20 תווים או פחות לכל אחד, אילו נתונים הספרייה Q המתוקנת צריכה לאמת, ומה האימות צריך לבדוק.
תשובת המודל היא כדלקמן.
| פריט | תמצית התשובה |
|---|---|
| נתונים לאימות | הערך שצוין ב-alg של כותרת ה-JWT |
| מה לאמת | שהוא אינו NONE |
כתיקון ישיר לפגיעות שתוארה בשאלה, זה נכון.
בפועל, אל תסתפקו ב”הכול מלבד NONE”
כאן צריך להפריד בין תשובת הבחינה לבין ההמלצה המעשית.
RFC 8725 קובע שספריית JWT צריכה לאפשר לקורא לציין קבוצת אלגוריתמים מותרים, וששום דבר מחוץ לקבוצה הזאת אינו רשאי לשמש.6 כלומר, הרעיון הוא זה.
גישה גרועה:
קבל אם token.header.alg != "none"
גישה טובה:
קבל רק אם זה כלול ב-serverConfig.allowedAlgorithms
לדוגמה allowedAlgorithms = ["RS256"]
דחייה של none בלבד עדיין יכולה להשאיר אלגוריתמים חלשים אחרים, או אפשרות של בלבול אלגוריתמים שבו סכימת מפתח ציבורי נחשבת בטעות לסכימת מפתח סימטרי. העיקרון הוא לא להוסיף תנאים שליליים למה לקבל, אלא לקבע קבוצה חיובית צרה של מה שמותר.
אימות JWT צריך לאשר לא רק את האלגוריתם אלא, לפי השימוש, לפחות גם את הבאים.
| פריט | מה לאשר |
|---|---|
| חתימה | האם אפשר לאמת במפתח ובאלגוריתם הצפויים |
iss |
האם זה מנפיק מהימן |
aud |
האם האסימון הונפק עבור ה-API הזה |
exp |
האם הוא בתוך תקופת התוקף |
nbf |
האם הוא אינו לפני זמן “לא תקף לפני” |
sub או מזהה משתמש |
האם זה נושא תקף ביישום |
| סוג אסימון | האם מבלבלים אסימון זהות עם אסימון גישה וכו׳ |
בשאלה הזאת שם המפתח במטען הוא user, אבל בפועל כדאי להשתמש ב-sub התקני או להגדיר בבירור את משמעות הטענה המותאמת.
flowchart TB
accTitle: אימות JWT בטוח מול לא בטוח
accDescr: אימות לא בטוח תלוי ב-alg, אימות בטוח משתמש ברשימת היתר בצד השרת
subgraph "אימות לא בטוח"
D1["קרא alg מכותרת ה-JWT"]
D2["קבל אם alg הוא none"]
D1 --> D2
end
subgraph "אימות בטוח"
S1["אלגוריתמים מותרים בתצורת השרת<br/>למשל RS256"]
S2["אשר ש-alg בכותרת ה-JWT<br/>נמצא ברשימת ההיתר"]
S3["אמת חתימה, iss, aud, exp"]
S1 --> S2 --> S3
end
איור 4: אימות בטוח מול לא בטוח. בפועל מקבעים קבוצה צרה של אלגוריתמים מותרים.
Base64url אינו הצפנה
יש אי-הבנה נפוצה נוספת לגבי JWT. הכותרת והמטען מיוצגים ב-base64url, אבל זו אינה הצפנה. כל אחד יכול לפענח ולקרוא אותם.
מה שהחתימה מבטיחה, ורק כשהאימות מצליח, הוא שהתוכן לא שונה מאז ההנפקה. זה אינו אומר שמידע אישי שרוצים לשמור בסוד מותר לשים במטען של JWT חתום.
7. שאלה 2(3) — גם עם JWT תקף, שינוי mid קרא נתונים של מישהו אחר
הבא הוא התקפה שאינה מזייפת את ה-JWT עצמו.
ה-API למשתמש מקבל מזהה משתמש בשם mid ב-GET או PUT. המודול המשותף P מביא או מעדכן, במסד הנתונים, את מידע המשתמש הקשור ל-mid הזה.
מבנה ההתקפה פשוט.
מזהה משתמש בתוך ה-JWT: user01 <- JWT חתום כהלכה
ה-mid של הבקשה: user02 <- שונה בידי התוקף
חתימת ה-JWT תקפה, לכן האימות מצליח. אבל ה-API סומך על mid=user02 כפי שניתן, ומחזיר את המידע של user02.
זה מקרה ספר של מה ש-OWASP API Security Top 10 2023 מכנה Broken Object Level Authorization (BOLA). בכל פעם שניגשים לנתונים באמצעות מזהה אובייקט שהמשתמש ציין, חייבים לבדוק הרשאה לאובייקט הספציפי הזה בכל פעם.7
flowchart LR
accTitle: התקפת BOLA
accDescr: שימוש ב-JWT תקף תוך שינוי mid של הבקשה למזהה משתמש אחר
A["תוקף"] -->|JWT user01<br/>mid user02| B["API משתמש"]
B -->|סומך על mid| C["מחזיר נתוני user02 ממסד הנתונים"]
איור 5: התקפת BOLA. האימות עובר, אבל ההרשאה מעולם לא נבדקה.
תשובת השאלה
קו תחתון 2 בטבלה 5 שואל, ב-40 תווים או פחות, על העיבוד שצריך להוסיף לקריאה למודול המשותף P.
תשובת המודל היא:
לוגיקה שמוודאת אם מזהה המשתמש הכלול ב-JWT תואם לערך של
mid
היתרון באימות בתוך המודול המשותף P הוא שאותה בדיקת הרשאה קלה ליישום גם ב-GET וגם ב-PUT, וגם ב-API עתידי שישתמש ב-P. העתקת אותה השוואה לכל מסך או נקודת קצה בנפרד פירושה שבמקום כלשהו היא תיעדר.
תכנון בטוח יותר הוא לא לקבל mid כלל
ל-API שמביא או מעדכן רק את המידע של הקורא עצמו, אין צורך לקבל מזהה משתמש מהלקוח כלל.
GET /users/me
Authorization: Bearer <JWT>
בצד השרת הנושא נשלף מה-JWT שכבר אומת.
principal = validateJwt(request.authorization)
userId = principal.subject
return repository.getUser(userId)
אותו דבר חל על עדכונים.
principal = validateJwt(request.authorization)
input = validateProfileUpdate(request.body)
repository.updateProfile(
userId = principal.subject,
name = input.name,
age = input.age
)
בדיקת השוואה מגנה עליכם אם כותבים אותה. אבל תכנון שלעולם אינו מקבל את מזהה היעד מבחוץ מצמצם את עצם קיום מחלקת הבאגים שבה שוכחים לכתוב את ההשוואה.
אם מנהל צריך להפעיל מידע של משתמש אחר, מפרידים כך.
PUT /users/me למשתמשים כלליים
PUT /admin/users/{userId} למנהלים
לנתיב המנהל דורשים הרשאות נפרדות, רישום ביקורת, ואם צריך — אימות מחדש. זה הופך את גבולות מדיניות ההרשאה לנראים הרבה יותר מאשר “הוסיפו חריג ל-API של המשתמש הכללי למנהלים בלבד”.
flowchart LR
accTitle: איך למנוע BOLA
accDescr: במקום להשתמש ב-mid של הבקשה, קובעים או בודקים את היעד מתוך ה-subject של ה-JWT
A["משתמש"] -->|GET /users/me + JWT| B["API"]
B -->|קח sub מה-JWT| C{"אם mid קיים<br/>האם הוא תואם ל-sub"}
C -->|תואם| D["החזר נתונים עצמיים"]
C -->|לא תואם| E["דחה ב-403"]
B -->|אין mid| F["חיפוש במסד לפי JWT sub"]
איור 6: איך למנוע BOLA. לא מקבלים mid, או בודקים מול ה-subject של ה-JWT.
הבחנה בין אימות להרשאה במשפט אחד
גם בבחינה וגם בפועל, הניסוח הבא עוזר.
- אימות: מי אתה
- הרשאה: מה מותר לאותו אדם לעשות
הצלחה באימות חתימת JWT מגיעה רק עד “אפשר לסמוך על הסובייקט שהאסימון הזה מייצג”. האם “לאותו סובייקט מותר לקרוא את user02” חייבים לאשר בנפרד.
8. שאלה 2(4) — status=paid הוא פגם בהרשאה ברמת המאפיין
מפרט ה-API למשתמש מגדיר את הבאים כפרמטרי עדכון.
mid מזהה משתמש
name שם
age גיל
אולם המאבחן הוסיף את הערך הבא, שאינו במפרט.
status=paid
סטטוס של משתמש בשכבת חינם השתנה אז לזה של משתמש משלם.
לפי השאלה, שירות L לא אימת את הפרמטרים שקיבל; הוא העביר את כולם ישר למודול המשותף P, שנבנה כך שיוכל לעדכן את מסד הנתונים ישירות.
התשובה לריק c היא המודול המשותף P.
flowchart TB
accTitle: Mass Assignment
accDescr: status=paid מחוץ למפרט מתווסף ומוחל בשלמות על האובייקט הפנימי
A["מפרט API<br/>mid / name / age"] -->|התוקף מוסיף status=paid| B["גוף הבקשה"]
B -->|קשירה אוטומטית| C["מודול משותף P"]
C -->|נשמר במסד| D["סטטוס החיוב השתנה ל-paid"]
איור 7: Mass Assignment. מאפיין מחוץ למפרט מוחל בשלמות על האובייקט הפנימי.
ההבדל מ-BOLA
החלפת ה-mid בפרק הקודם והוספת ה-status הזאת נראות דומות, אבל רמת הפירוט המוגנת שונה.
| פגיעות | מה שהתוקף משנה | מה שצריך לבדוק בפועל |
|---|---|---|
החלפת mid |
אובייקט היעד | האם למשתמש הזה מותר לגשת לרשומת המשתמש הזאת |
הוספת status |
מאפיין בתוך האובייקט | האם למשתמש הזה מותר לשנות את השדה הזה |
OWASP API Security Top 10 2023 מטפל באחרון כ-Broken Object Property Level Authorization, ומכניס לכאן את מה שנקרא בעבר Mass Assignment.8
“לשים JSON ישר בישות” מסוכן
המימוש הפגיע, באופן מושגי, נראה כך.
entity = repository.find(body.mid)
bindAllProperties(entity, body)
repository.save(entity)
גם אם במסך יש רק שדות קלט ל-name ול-age, תוקף יכול להרכיב את בקשת ה-HTTP ישירות. היעדר שדה מהממשק אינו גבול אבטחה.
מימוש בטוח הופך את השדות הניתנים לעדכון למפורשים.
input = parseExactSchema(body, fields = ["name", "age"])
entity = repository.find(authenticatedUserId)
entity.name = input.name
entity.age = input.age
repository.save(entity)
שני דברים חשובים כאן.
- סוג הקלט לעדכון צריך להחזיק רק את השדות שמותר למשתמש לשנות.
- במקום להתעלם בשקט משדות לא מוכרים מחוץ למפרט, דחו אותם כשגיאה אם אפשר.
התעלמות שקטה משדות לא מוכרים מסתירה את העובדה שההתקפה נכשלה, אבל גם מאפשרת לפספס טעויות מימוש של הלקוח וסימני התקפה. אלא אם יש סיבת תאימות שלא לעשות זאת, דחייה בסכימה מחמירה מקלה על החקירה.
flowchart TB
accTitle: הרשאה ברמת המאפיין
accDescr: DTO העדכון מחזיק רק רשימת היתר, ומאפיינים לא מוכרים נדחים
subgraph "DTO עדכון - רשימת היתר"
D1["name"]
D2["age"]
end
A["גוף הבקשה"] -->|אימות סכימה| B{"רק שדות<br/>מותרים קיימים"}
B -->|כן| C["עדכן name/age של הישות"]
B -->|לא| D["החזר שגיאה"]
E["שירות תשלום<br/>התראה מאומתת"] -->|נתיב ייעודי| F["עדכן status=paid"]
איור 8: הרשאה ברמת המאפיין. מגבילים שדות לעדכון ברשימת היתר, ומשנים סטטוס חיוב רק בנתיב נפרד.
status צריך להשתנות רק מתוצאת התשלום
status=paid אינו חלק מפרופיל המשתמש. זה מצב שנגזר מעובדה בצד השרת: שהתשלום הצליח.
עדכון פרופיל משתמש
-> רק name / age ניתנים לשינוי
התראה מאומתת משירות התשלום
-> התאם paymentId
-> מנע עיבוד כפול
-> שנה status ל-paid
גם כשנשמר באותה עמודת מסד נתונים, הסמכות לשנות אותו והנתיב שמשמש לשינוי הם דברים שונים. שימוש בישות פנימית ישירות כסוג קלט של API חיצוני מוחק את הגבול הזה.
9. שאלה 2(5) — הגנות מפני ניחוש כוחני מחזיקות את ספירת הכשלונות כמצב
לניחוש הכוחני של הקוד בן ארבע הספרות, הריק d בטבלה 5 שואל, ב-30 תווים או פחות, על העיבוד ששייך לשם. הסף הוא 10.
תשובת המודל היא:
לוגיקה שנועלת את החשבון ברגע שמספר הכשלונות הרצופים חורג מהסף
זה אינו סותר את הסטטלס משאלה 1. אי-החזקת מצב השיחה של קריאת API כשיחת שרת, ושימור ספירת הכשלונות הנחוצה להחלטת אבטחה, הם שני דברים שונים.
flowchart LR
accTitle: עם הגבלת קצב ניסיונות ובלי
accDescr: בלי הגבלה הקוד נפרץ בממוצע ב-500 שניות, אבל הגבלת ספירת כשלונות מאטה בחדות את ההתקפה
subgraph "בלי הגבלה"
A1["10 ניסיונות לשנייה"] -->|כ-500 שניות| B1["האימות מצליח"]
end
subgraph "עם הגבלה"
A2["נעילה אחרי 10 כשלונות"] -->|מהירות ההתקפה קורסת| B2["החשבון נעול"]
C2["השהיה מדורגת"] --> B2
end
איור 10: עם הגבלת קצב ניסיונות ובלי. הגבלת ספירת כשלונות יכולה לעצור ניחוש כוחני באופן מעשי.
בפועל, אל תסתמכו רק על נעילה קבועה
הגבלת ניסיונות לכל חשבון נחוצה, אבל אם תוקף יודע מזהה משתמש של מישהו אחר, הוא יכול להיכשל במכוון 10 פעמים כדי לנעול את המשתמש הלגיטימי. לכן בפועל משלבים את הבאים.
| בקרה | תפקיד |
|---|---|
| ספירת כשלונות לכל חשבון | עוצרת ניחוש כוחני מול חשבון יחיד |
| זמני המתנה מדורגים | סובלים טעויות הקלדה של משתמש לגיטימי תוך האטת ההתקפה |
| בקרות לפי IP מקור, מכשיר, ASN וכו׳ | מדכאות התקפות שמנסות מעט פעמים מול חשבונות רבים |
| החלטות מבוססות סיכון | מחילות הגבלות חזקות יותר לאזורים, מכשירים או מהירויות חריגים |
| הודעה למשתמש | מאפשרת למשתמש להבחין בהתקפה או בטעות שלו |
| הליך שחזור בטוח | מונע מערוץ השחרור עצמו להפוך לנתיב התקפה |
יתר על כן, כשקוד נשלח מחדש, אסור לאפס את ספירת הכשלונות לאפס — אחרת תוקף יכול למלא מחדש את תקציב הניסיונות בכל קריאה ל-API השליחה מחדש. גם NIST SP 800-63B הנוכחי דורש שספירת הכשלונות לא תאופס גם כשנוצר סוד אימות חדש.4
הפכו את קוד האימות לחד-פעמי
השאלה מתמקדת בזמן התפוגה, אבל בפועל נחוצים גם הבאים.
- בטלו מיד קוד שהצליח.
- דחו שימוש חוזר באותו קוד.
- אל תשאירו את הקוד עצמו ביומנים.
- עשו את התגובה כך שלא ניתן להסיק מהצלחה או כישלון של בדיקת הקוד אם משתמש קיים.
- שימו הגבלת ניסיונות גם על ה-API ששולח את הקוד.
כל עוד משתמשים בסוד קצר, אי אפשר להשאיר את האבטחה ליצירה אקראית בלבד.
flowchart TB
accTitle: הגנות לקוד אימות
accDescr: מעבר למספר ספרות ותפוגה, מגנים בהגבלת ניסיונות, דחיית שימוש חוזר, התראה ועוד
A["קוד אימות"] --> B["הגדלת מספר הספרות"]
A --> C["קיצור התפוגה"]
A --> D["הגבלת קצב ניסיונות"]
A --> E["ביטול אחרי הצלחה"]
A --> F["אין איפוס ספירת כשלונות בשליחה מחדש"]
A --> G["אין להשאיר את הקוד ביומן"]
A --> H["בקרה לפי מקור"]
איור 11: הגנות לקוד אימות. משלבים מספר ספרות ותפוגה עם בקרת ניסיונות ופרקטיקות הפעלה.
10. הבחנה בין ארבעת חלקי שאלה 2 בעמוד אחד
הנקודות בשאלה 2 שקל לערבב, מסודרות לפי הערך שהתוקף שלט בו.
| התקפה | הערך שהתוקף שינה | במה לא היה צריך לסמוך | תיקון שורש |
|---|---|---|---|
| זיוף JWT | ה-alg בכותרת ה-JWT, מזהה המשתמש במטען |
אלגוריתם האימות שהאסימון עצמו מכריז עליו | קיבוע האלגוריתמים המותרים בצד השרת |
| קריאת מידע של משתמש אחר | ה-mid של הבקשה |
מזהה היעד שציין הלקוח | התאמה מול ה-subject של ה-JWT, או קביעת מזהה היעד מה-JWT |
| שדרוג למשתמש משלם | status מחוץ למפרט |
כל המאפיינים שנקשרים אוטומטית | הפיכת מאפיינים לעדכון לרשימת היתר |
| פריצת הקוד בן 4 הספרות | מועמדים ל-otp |
ניסיונות אימות בלי הגבלה | הוספת הגבלות קצב, השהיות והחלטות סיכון |
חשוב לא לאגד את הכול כ”אימות הקלט”.
algהוא מדיניות קריפטוגרפית.midהוא הרשאה ברמת האובייקט.statusהוא הרשאה ברמת המאפיין.otpהוא עמידות לניחוש מקוון.
גם בתוך אותה בקשת HTTP, הסיבה שכל אחד מהם חייב להיות מוגן שונה.
11. שאלה 3(1) — אישור הרצת קוד מרחוק בלי לגרום נזק
אחרי השקת השירות מתפרסמת פגיעות קריטית V בספרייה H, ספריית קוד פתוח נפוצה. רצף האירועים בשאלה הוא כדלקמן.
- התוקף שם מחרוזת שמכילה JNDI Lookup בכותרת HTTP ושולח אותה.
- שרת היעד רושם את הערך הזה ביומן.
- הספרייה הפגיעה מעריכה את ה-JNDI Lookup ושואלת את שרת ה-LDAP של התוקף.
- תשובת ה-LDAP מחזירה את כתובת שרת ה-HTTP של התוקף.
- שרת היעד שולף את קובץ המחלקה ומריץ את הפקודה.
כששם המוצר הספציפי מוסתר, זה נקרא כהתקפה מסוג Log4Shell (CVE-2021-44228). גם תיאור Apache עצמו מתאר את הפגיעות ככזאת שבה, אם תוקף שולט בהודעות יומן או בפרמטרים, הוא יכול להריץ קוד שרירותי שנטען משרת LDAP.9
flowchart LR
accTitle: זרימת אישור לפגיעות מסוג Log4Shell
accDescr: שימוש בקריאה חוזרת בלתי מזיקה כדי לאשר אם השרשרת מ-JNDI להרצת קוד מרחוק באמת עוברת
A["תוקף"] -->|הזרקת מטען jndi/ldap<br/>ל-x-api-version| B["שרת פגיע"]
B --> C["עיבוד יומן"]
C -->|JNDI Lookup| D["שרת LDAP זדוני"]
D -->|תשובת URL של HTTP| E["שרת HTTP זדוני<br/>index.html"]
E -->|רישום ה-GET| F["שרת בדיקה<br/>יומן גישה"]
F -->|אישור נגישות| G["הפגיעות אושרה"]
איור 12: זרימת אישור לפגיעות מסוג Log4Shell. הנגישות מאושרת ברישום גישת HTTP ולא בהוצאת פקודה הרסנית.
קוד האימות מפעיל רק גישת HTTP בלתי מזיקה
חברה G מריצה קוד אימות שאין לו השפעה על המערכת, כדי לאשר אם אפשר לנצל את פגיעות V מבחוץ. הפקודה היחידה שקוד האימות מוציא היא שליפת index.html של שרת הבדיקה.
שאלה 3(1) שואלת מה צריך לממש בשרת הבדיקה כדי לאשר שהפקודה הורצה.
תשובת המודל היא:
מנגנון שרושם ומאפשר לאשר גישות ל-index.html של שרת הבדיקה
אם יומן הגישה של שרת הווב רושם GET משרת היעד, זה מאשר לפחות שהשרשרת הבאה עברה.
בקשת HTTP חיצונית
-> עיבוד יומן
-> JNDI Lookup
-> תשובת LDAP
-> שליפת מחלקה
-> הרצת פקודת אימות
-> גישת HTTP לשרת הבדיקה
למה “הצגת טקסט על המסך” אינה מספיקה
יעד ההתקפה הוא השרת. אין ערובה שמשהו משתנה במסך הדפדפן של המשתמש. גם, גם במקום שבו הפגיעות קיימת, התקשורת היוצאת באמצע הדרך יכולה להיחסם בחומת אש.
רישום הגישה בצד שרת הבדיקה מייצר ראיה שניתן לצפות בה ששרת היעד באמת הגיע לעולם החיצון.
כשמבצעים אימות מהסוג הזה בפועל, תמיד שומרים על הבאים.
- קבלו הרשאה מפורשת מבעל מערכת היעד.
- השתמשו בשיטת אימות שאין לה השפעה על הייצור, או שהשפעתה קטנה ומקובלת.
- אל תשתמשו בפקודות הרסניות כמו כתיבה, מחיקה או שינויי תצורה.
- נהלו בעצמכם את דומיין האימות ואת השרת.
- רשמו את זמן האימות, המקור, היעד והקריאה החוזרת הצפויה.
- פרקו שרתי LDAP או HTTP זמניים ופרטי גישה אחרי האימות.
“לאשר שאפשר להריץ קוד שרירותי” ו”להריץ קוד מסוכן שרירותי” אינם אותו דבר. שמרו את תופעות הלוואי במינימום שנחוץ לעמידה במטרה.
12. שאלה 3(2)(3) — ה-WAF בודק את כותרת ה-HTTP
ה-WAF של שירות N מאפשר לבחור GET, POST, PUT, ANY, Header, COOKIE או Multipart כיעד בדיקה.
קוד ההתקפה נכנס לערך של כותרת HTTP בשם x-api-version. לכן הריקים e ו-f בטבלה 6 שניהם Header.
ממפים את המיקום שניתן בטקסט ישירות ליעד הבדיקה של ה-WAF
זה פחות ידע כללי ויותר קריאה של זרימת הנתונים בטקסט השאלה.
היכן מונחת מחרוזת ההתקפה:
כותרת x-api-version
|
v
יעד הבדיקה של ה-WAF:
Header
זה אינו פרמטר GET ואינו גוף POST. במקום להביט ברשימת היכולות של ה-WAF ולבחור ANY כי “זה נראה כמו התקפה”, עונים במיקום שבו טקסט השאלה אומר שהתוקף שם את הערך.
טיפול בהחלפת רישיות
ההצעה הראשונה הייתה, באופן מושגי, הכלל הבא.
Header \Wjndi\W חסום
Header \Wldap\W חסום
אבל החלפת רישיות, כמו jNdI, מתחמקת מתבנית שמתאימה רק לאותיות קטנות.
תשובת המודל לשאלה 3(3) היא אחת מהבאות.
\W[jJ][nN][dD][iI]\W
\W(j|J)(n|N)(d|D)(i|I)\W
בחוברת השאלות הלוכסן האחורי יכול להיראות כמו סימן ין בגליף של הסביבה היפנית, אבל כביטוי רגולרי זה \W. \W מתאים לכל תו שאינו אות, ספרה או קו תחתון. בתחביר JNDI Lookup מופיעים תווים שאינם מילה כמו ${ ו-: מיד לפני ואחרי jndi, והתבנית כתובה כדי לתפוס גם אותם.
אפשר ליישם את אותו רעיון כדי להפוך גם את צד ה-ldap לבלתי רגיש לרישיות.
\W[lL][dD][aA][pP]\W
אל תתייחסו לביטוי הרגולרי הזה כאל “הגנת Log4Shell שלמה”
הבחינה מבקשת ביטוי רגולרי שמטפל בטכניקת ההתחמקות שמוצגת בטקסט השאלה. התקפות בעולם האמיתי יכולות לכלול פיצול מחרוזות, Lookup חלופי, קידוד, פרוטוקולים אחרים ווריאציות אחרות שקשה לכסות בחתימות בלבד.
לכן המיקום המעשי הוא כדלקמן.
- חסמו באופן זמני דפוסי התקפה ידועים כעת עם ה-WAF.
- בדקו אם הספרייה שנפגעה באמת קיימת.
- הגבילו תעבורה יוצאת של LDAP, RMI ו-HTTP מיותר.
- עדכנו לגרסה מתוקנת.
- אחרי העדכון, עדיין בדקו את היומנים וחקרו אם אירעה פריצה.
ה-WAF הוא שכבה שקונה זמן עד שגרסה מתוקנת זמינה.
flowchart TB
accTitle: מקומו של ה-WAF
accDescr: ה-WAF הוא שכבת הקלה זמנית, תיקון השורש הוא עדכון הספרייה לגרסה מתוקנת
A["פורסמה פגיעות קריטית"] --> B["אישור השפעה"]
B --> C["הקלה זמנית"]
C -->|כלל WAF<br/>זיהוי/חסימה| D["עצירת דפוס ההתקפה באופן זמני"]
C -->|הגבלת תעבורה יוצאת| E["סגירת נתיב הניצול"]
D --> F["עדכון לספרייה המתוקנת"]
E --> F
F --> G["סקירה לאחר מעשה ומניעה"]
איור 13: מקומו של ה-WAF. ה-WAF רק קונה זמן עד שמגיע תיקון; תיקון השורש הוא העדכון.
13. שאלה 3(4) — למה להתחיל ב”זיהוי”
לגבי כלל ה-WAF המעודכן, Z, מומחה אבטחה רשום, מייעץ להגדיר את המצב ל”זיהוי” ולא ל”חסימה” לתקופה קבועה אחרי שהוא עולה לייצור.
השאלה שואלת, ב-25 תווים או פחות לכל אחד, על יתרון השימוש במצב זיהוי ועל מה שצריך לעשות כדי למזער נזק.
תשובת המודל היא:
| פריט | תמצית התשובה |
|---|---|
| יתרון | יכול למנוע חסימה שנגרמת מהתראת שווא |
| מה לעשות | לבדוק אם זו התקפה בכל פעם שמתקבלת התראה |
מצב זיהוי אינו מצב “אל תעשה כלום”
במצב זיהוי, תעבורה שתואמת לכלל עדיין עוברת, אבל היא נרשמת ומופעלת התראה. גם אם המחרוזת jndi או ldap מופיעה במקרה בתוך קריאת API לגיטימית, העסק אינו נעצר מיד.
בתמורה, צד ההפעלה צריך לעשות את הבא.
התקבלה התראה
|
v
בדוק את הבקשה הנדונה
|
+-- תעבורה לגיטימית -> צמצם את הכלל, שקול חריג
|
+-- התקפה -> בודד את היעד, שמר יומנים, חקור השפעה, עבור לחסימה
אם אף אחד לא מביט בהתראות, למצב זיהוי אין כלל אפקט הגנתי. זיהוי עובד רק בצמד עם תהליך הפעלה שצופה ושופט.
הנתיב מזיהוי לחסימה
הליך השקה טיפוסי הוא כדלקמן.
- הריצו מצב זיהוי מול תעבורה אמיתית.
- סווגו פגיעות כהתראות שווא או כחיוביות אמיתיות.
- כוונו את כותרת היעד, הנתיב, ה-API, גבולות מילים וכן הלאה.
- אשרו שההשפעה על תעבורה לגיטימית מקובלת.
- עברו למצב חסימה.
- עקבו אחרי מספר החסימות וההשפעה העסקית.
זה, עם זאת, העיקרון לזמנים רגילים. כשפגיעות קריטית, מנוצלת בפועל, ואין חלופה, אפשר לשפוט שההשבתה שנגרמת מפריצה עולה על זו שנגרמת מהתראת שווא, ולבחור בחסימה מההתחלה. בתרחיש הבחינה בוחרים קודם במצב זיהוי כדי לאשר שהשירות יכול להמשיך לפעול כמו קודם.
flowchart LR
accTitle: ממצב זיהוי של WAF למצב חסימה
accDescr: צופים בהתראות במצב זיהוי, מכוונים התראות שווא, ואז עוברים למצב חסימה
A["מצב זיהוי"] -->|תעבורה אמיתית| B["הופעלה התראה"]
B --> C{"התקפה או<br/>התראת שווא"}
C -->|התראת שווא| D["כוונון הכלל"]
D --> A
C -->|התקפה| E["מעבר למצב חסימה"]
E --> F["מעקב אחרי מספר חסימות והשפעה עסקית"]
איור 14: מזיהוי לחסימה. קודם צופים ומכוונים, מאשרים שההשפעה מקובלת, ואז עוברים לחסימה.
14. ה-WAF הוא אמצעי זמני; העדכון הוא תיקון השורש
בשאלה, באתר הרשמי של ספרייה H לא היה עדיין תיקון וגם לא פתרון זמני, ואפילו כלל ה-WAF המקיף של ספק הענן היה אמור לקחת עד 72 שעות. לכן חברה G מאשרת את ההשפעה בעצמה וחוסמת באופן זמני לפחות את הדפוסים שכבר זוהו.
הרצף הזה הוא הצורה הבסיסית של תגובה לאירוע.
| שלב | מטרה | התגובה בשאלה זו |
|---|---|---|
| אישור השפעה | לשפוט אם הארגון עצמו באמת בסיכון | אישור ניצול מבחוץ בקריאה חוזרת בלתי מזיקה |
| הקלה זמנית | לקנות זמן עד שיגיע תיקון | כללי WAF, זיהוי/חסימה, הגבלות תעבורה יוצאת |
| תיקון שורש | להסיר את הסיבה הפגיעה | עדכון לספרייה מתוקנת |
| סקירה לאחר מעשה | לבדוק אם כבר נוצלה | חקירת יומני WAF, יישום, DNS, פרוקסי ואחרים |
| מניעת הישנות | להאיץ את ההחלטה הבאה | מלאי תלויות, SBOM, הליך עדכון, נתיב קשר |
“אנחנו לא יודעים אם אנחנו משתמשים בזה” הוא מקור העיכוב הגדול ביותר
בשאלה, גם כשחברה G שואלת את חברה F אם היא משתמשת בספרייה H, התשובה לוקחת זמן כי נדרש ניתוח תצורה מפורט.
בפועל, אם מתחילים לחפש קובצי JAR רק אחרי שפורסמה פגיעות קריטית, התגובה מתעכבת. כדאי שיהיו לפחות הבאים במצב שגרה.
- מלאי תלויות ישירות וטרנזיטיביות.
- הרכיבים והגרסאות שכלולים בפועל בארטיפקטים.
- לאילו שירותים, מכולות ומכשירים הם פרוסים.
- הליך לעדכון ספרייה תלויה ולבנייה מחדש/הפצה מחדש.
- נתיב קשר לאישור שינויי חירום.
- היעדים המותרים לתעבורה יוצאת, וההשפעה של חסימתם.
- היכן נשמרים יומנים ואיך לחפש בהם.
SBOM אינו המטרה עצמה. הוא אינדקס כדי לענות, בזמן קצר, “אילו מערכות רצות מושפעות מהפגיעות הזאת”.
אל תפסיקו לחקור ברגע שעדכנתם
ייתכן שכבר הותקפתם בסביבות זמן פרסום הפגיעות. עדכון לגרסה מתוקנת עוצר ניצול עתידי, אבל אינו מוחק פרטי גישה שכבר נפרצו או דלת אחורית שכבר ניטעה.
לפגיעות מסוג Log4Shell, חוקרים לפחות את הזוויות הבאות.
- בקשות HTTP שמכילות מחרוזות חשודות המצביעות על JNDI או LDAP.
- תקשורת משרת היישום החוצה ל-LDAP, RMI או HTTP חיצוניים.
- השקת תהליכי בן חריגים.
- יצירת JAR, מחלקות, סקריפטים או קבצים להרצה חשודים.
- גישה לפרטי גישה בענן או למשתני סביבה.
- אימות, שינויי הרשאות והעברות יוצאות בסביבות זמן העדכון.
חשוב לא להסיק “לא הותקפנו” מיומני WAF בלבד. יש נתיבים פנימיים שלעולם אינם עוברים ב-WAF, ויומנים שלא נשמרו בעבר.
15. דרך קריאה שמקלה להרוויח נקודות בבחינה
השאלה הזאת פחות מבחן ידע ויותר תרגיל בקריאת הפער בין מפרט למימוש.
15.1 הפרידו “מפרט” מ”מימוש” בטבלאות
בסוגיית ה-status, ערך שנעדר ממפרט ה-API עובר במימוש.
מפרט:
mid / name / age
מימוש:
שלח את כל הפרמטרים שהתקבלו ל-P
ברגע שרואים את הפער הזה, מתברר שהריק c הוא המודול המשותף P.
15.2 סמנו קו מתחת לערך שהתוקף שינה
הערך ששונה בכל התקפה הוא כדלקמן.
- ה-
algבכותרת ה-JWT - מזהה המשתמש במטען ה-JWT
- פרמטר ה-API
mid statusמחוץ למפרט- ה-
otpשל ה-API לאימות - כותרת ה-HTTP
x-api-version
כמעט כל שאלה שואלת “היכן צריך לאמת את הערך הזה”.
15.3 החזירו את התשובה למונחי טקסט השאלה עצמו
בפועל אפשר לקרוא לאלה “BOLA”, “Mass Assignment” ו-“rate limiting”. אבל מה שהשאלה מבקשת הוא עיבוד קונקרטי שמותאם למבנה טקסט השאלה.
דוגמה גרועה:
בצעו הרשאה כראוי.
דוגמה טובה:
ודאו אם מזהה המשתמש הכלול ב-JWT תואם לערך של mid.
דוגמה גרועה:
נקטו הגנות מפני ניחוש כוחני.
דוגמה טובה:
נעלו את החשבון ברגע שמספר הכשלונות הרצופים חורג מהסף.
ידיעת השם המופשט לבדה אינה מייצרת תשובה שניתן לנקוד בתוך מגבלת התווים.
15.4 ב-WAF, עקבו אחרי “היכן זה הונח”
יעד הבדיקה של ה-WAF נקבע לא בניחוש מסוג ההתקפה, אלא מהמיקום שבו הונחה מחרוזת ההתקפה.
הונח בכותרת x-api-version
↓
יעד הבדיקה הוא Header
הערה בפירוט הציונים ששיעור התשובות הנכונות בשאלה 3(1) היה נמוך במקצת נובעת גם מכך שהרבה תשובות לא תאמו לזרימת ההתקפה באיור 6. עצם ציור מחדש של רצף ההתקפה עם חצים כבר חושף מה צריך לצפות.
16. רשימת בדיקה לסקירות API בעולם האמיתי
רשימת בדיקה להחזרת השאלה הזאת לתכנון ולסקירות קוד בפועל.
אימות JWT
- אלגוריתם החתימה המותר מקובע בתצורת השרת.
noneואלגוריתמים בלתי צפויים נדחים.- חתימה,
iss,aud,expו-nbfמאומתים לפי השימוש. - אסימוני זהות, אסימוני גישה ואסימוני רענון אינם מתבלבלים זה בזה.
- יש הליך לסיבוב מפתחות ולביטול.
- מידע שחייב להישאר חסוי אינו מוכנס למטען ה-JWT.
הרשאה ברמת האובייקט
- שינוי המזהה בתוך בקשה אינו יכול להגיע לנתוני משתמש אחר.
- ההרשאה נאכפת ברשימה, בפירוט, בעדכון, במחיקה ובהורדה כאחד.
- ההרשאה נאכפת בשכבה משותפת שמגיעה לנתונים, לא במסך.
- ל-API ליחיד בלבד, שקלתם לגזור את מזהה היעד מהאסימון במקום.
- פעולות מנהל משתמשות במדיניות נפרדת מ-API המשתמש הכללי.
הרשאה ברמת המאפיין
- סוג הקלט החיצוני וישות מסד הנתונים נשמרים נפרדים.
- שדות לעדכון מנויים כרשימת היתר.
- מאפיינים מחוץ למפרט נדחים או מבוקרים.
- מצב כמו הרשאות, חיוב, אישור ובעלות אינו ניתן לשינוי מקלט משתמש.
- גם התשובות מדירות מאפיינים חסויים מיותרים.
ניסיונות אימות
- יש הגבלת ספירת כשלונות לכל חשבון.
- יש השהיה מדורגת ובקרה לפי מקור.
- ספירת הכשלונות אינה מתאפסת כשהקוד מונפק מחדש.
- קוד האימות ניתן לשימוש פעם אחת בלבד.
- קודי אימות וסיסמאות אינם נשארים ביומנים.
- הליך השחרור/השחזור אינו עצמו נתיב אימות חלש יותר.
פגיעויות קריטיות בספריות תלויות
- אפשר למפות שירותים רצים לגרסאות התלויות שלהם.
- יש הליך לאימות השפעה בשיטה בלתי מזיקה.
- אפשר ליישם אמצעים זמניים כמו כללי WAF והגבלות יוצאות.
- יש תהליך הפעלה שבו אדם אחראי סוקר התראות זיהוי.
- יש נתיב שחרור חירום לעדכון לגרסה מתוקנת.
- בודקים יומנים לאפשרות ניצול לפני העדכון.
17. שני הפנים של רכיבים משותפים, כפי שנראים דרך השאלה הזאת
בשאלה מופיעים שני רכיבים משותפים: ספריית ניהול JWT Q והמודול המשותף P.
לרכיבים משותפים יש יתרונות גדולים.
- תיקון במקום אחד מתפשט לכל API שמשתמש בו.
- לוגיקת הרשאה ואימות אינה חייבת להיות משוכפלת לכל יכולת.
- אפשר לרכז יעדי בדיקה.
- אפשר לאחד פורמטי יומן וביקורת.
מצד שני, גם טעויות מתפשטות בכל המערכת.
- אם ספרייה Q מקבלת
alg=none, כל API שמשתמש ב-JWT הופך לפגיע. - אם המודול המשותף P מקבל
midאוstatusשרירותיים, גם GET וגם PUT הופכים לפגיעים. - אם ספרייה H הפגיעה משמשת ביסוד, כל נתיב שרושם כותרות HTTP הופך לחלק ממשטח ההתקפה.
לכן מה שצריך לשתף אינו גישה לנתונים בלבד. צריך לשתף את אינוריאנטי האבטחה עצמם, ולאמת בקפדנות את הרכיב המשותף הזה בנפרד.
למשל, עשו את החוזה של P כך.
P.getOwnUser(authenticatedSubject)
P.updateOwnProfile(authenticatedSubject, ProfileUpdate{name, age})
בטוח יותר לא לחשוף API ברמה נמוכה כמו הבא ישירות לקוראים רגילים.
P.getUser(arbitraryMid)
P.updateUser(arbitraryMap)
האחרון נחוץ רק לקבוצה מוגבלת של נתיבים, כמו עיבוד ניהולי. מסירת החופש ברמה הנמוכה הזאת לכל API מניבה תכנון שתלוי בכך שכל קורא ישתמש בו נכון, בכל פעם.
18. סיכום
שאלת PM 1 באביב 2024 (רייווה 6) היא שאלה שקוראת ומפרידה את נושאי אבטחת ה-API אחד-אחד.
שימוש ב-JWT אינו אומר שהאימות בטוח. מתן בחירת אלגוריתם החתימה לתוקף מאפשר לכתוב מחדש את מזהה המשתמש.
חתימת JWT תקפה אינה אומרת שההרשאה נכונה. אמון ב-mid של הבקשה מאפשר למשתמש לגיטימי לגשת למידע של מישהו אחר.
היכולת לעדכן את האובייקט שלך אינה אומרת שמותר לשנות כל מאפיין. קשירה אוטומטית של מצב פנימי כמו status מאפשרת לכתוב מחדש הרשאות או סטטוס חיוב.
קיום תפוגה לקוד האימות אינו אומר שהוא עמיד לניחוש כוחני. צריך לחשב את מרחב המועמדים ואת מהירות הניסיונות, ולהגביל את מספר הכשלונות.
הכנסת כלל ל-WAF אינה אומרת שהפגיעות תוקנה. זיהוי וחסימה רק קונים זמן; מאשרים את ההשפעה ובסופו של דבר מעדכנים את הספרייה.
flowchart TB
accTitle: מיפוי פגיעויות להגנות
accDescr: ממפה כל פגיעות לגבול האמון שלה ולהגנה עליה
A["זיוף JWT"] -->|אימות אסימון| B["קיבוע האלגוריתם המותר"]
C["החלפת mid"] -->|הרשאה ברמת האובייקט| D["התאמה מול subject של JWT/אין צורך ב-mid"]
E["status=paid"] -->|הרשאה ברמת המאפיין| F["הפיכת DTO העדכון לרשימת היתר"]
G["ניחוש כוחני של קוד בן 4 ספרות"] -->|בקרת ניסיונות אימות| H["הגבלת כשלונות/השהיה"]
I["פגיעות מסוג Log4Shell"] -->|מקלט לביצוע| J["עדכון ספרייה/WAF"]
איור 15: מיפוי פגיעויות להגנות. מפרידים את התיקון לפי הגבול שנשבר.
עיקרון אחד עובר בכל השאלה הזאת.
לעולם אל תהפכו הצלחה בבדיקה הקודמת לסיבה לדלג על גבול האמון הבא.
מאמרים קודמים בסדרה מכסים את פגיעות ה-XSS המאוחסן בשאלת PM 1 בסתיו 2023 (רייווה 5) ואת הוצאת הנתונים מ-Wi-Fi לאורחים בשאלת PM 2 בסתיו 2023 (רייווה 5). למבט על מה לבדוק באתר כולו, ראו גם שימוש ב-“How to Secure Your Website” של IPA כרשימת בדיקה.
flowchart TB
accTitle: סיכום סופי
accDescr: מראה שהצלחה בבדיקה הקודמת לעולם אינה סיבה לדלג על גבול האמון הבא
A["האימות מצליח"] --> B["אימות חתימת JWT"]
B --> C["הרשאה ברמת האובייקט"]
C --> D["הרשאה ברמת המאפיין"]
D --> E["הגבלת קצב ניסיונות"]
E --> F["גבול מקלט לביצוע"]
F --> G["WAF/עדכון ספרייה"]
איור 16: סיכום סופי. גבולות אמון נבדקים בשלבים, ואסור לדלג על אף אחד.
מקורות
-
IPA, Spring 2024 (Reiwa 6) Registered Information Security Specialist Examination, PM Question Booklet. טקסט השאלה שעליו מבוסס המאמר. ↩
-
IPA, Spring 2024 (Reiwa 6) Registered Information Security Specialist Examination, PM Model Answers. תשובת המודל הרשמית לכל שאלה. ↩
-
IPA, Spring 2024 (Reiwa 6) Registered Information Security Specialist Examination, PM Grading Commentary. הסבר של שיעורי תשובות נכונות וטעויות נפוצות. ↩
-
NIST, SP 800-63B: Authentication and Authenticator Management. קובע מספרי ספרות לסודות קצרי טווח, הגבלות קצב ניסיונות, ספירת הכשלונות בהנפקה מחדש, ואי-שימוש בדוא״ל לאימות מחוץ לערוץ, בין שאר הדרישות. ↩ ↩2
-
RFC Editor, RFC 7519: JSON Web Token (JWT). מפרט ה-JWT, כולל Unsecured JWT ו-
alg=none. ↩ -
RFC Editor, RFC 8725: JSON Web Token Best Current Practices. ה-BCP שקובע קיבוע קבוצת האלגוריתמים המותרים, ואימות המנפיק, הנושא והקהל, בין שאר הפרקטיקות. ↩
-
OWASP, API1:2023 Broken Object Level Authorization. מסביר את הצורך לבדוק הרשאה לכל מזהה אובייקט שהמשתמש מציין. ↩
-
OWASP, API3:2023 Broken Object Property Level Authorization. מסביר פגמים בהרשאה ברמת המאפיין, כולל Mass Assignment, ואת ההגנות עליהם. ↩
-
Apache Logging Services, Security. מסביר את השפעת CVE-2021-44228, הרצת קוד דרך JNDI ו-LDAP, והגרסאות המתוקנות. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מעמקי הווירטואליזציה של Windows (חלק 3) — מכונות וירטואליות שעולות בשניות: למה WSL2, Windows Sandbox וקונטיינרים כל כך קלים
למה WSL2 ו-Windows Sandbox עולים בשניות ומרגישים כל כך קלים? המאמר מסביר את המנגנונים, מתמונות בסיס דינמיות ומ-direct map דרך הקצאת זיכרו...
מעמקי הווירטואליזציה של Windows (חלק 2) — זיכרון שאפילו הליבה לא יכולה לראות: איך VBS, HVCI ו-Credential Guard עובדים
בהתקנה נקייה על חומרה תואמת, VBS מופעל כברירת מחדל ומשתמש בהיפרווייזור וב-SLAT כדי ליצור בידוד חזק מהליבה. המאמר מסביר את המבנה של VTL, S...
מעמקי הווירטואליזציה של Windows (חלק 1) — היכן Windows שלכם באמת רץ? ההיפרווייזור והמחיצות
כשמפעילים Hyper-V, Windows המארח עצמו רץ מעל ההיפרווייזור כמחיצת השורש. המאמר מסביר את יסודות הווירטואליזציה דרך התפקידים של VT-x, SLAT ...
Win32 Thread Pool API — מקביליות בלי ליצור תהליכונים, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד הנייטיב? המאמר מסביר את ה-API של מאגר התהליכונים של Win32 שעוצב מחדש ב-Vista — ארבעת האובייקטים work,...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון ועד אבטחה
מדריך מעשי ל-Named Pipes, התקשורת הסטנדרטית בין תהליכים ב-Windows. המאמר מסדר ממקורות ראשוניים את הבחירה בין מצב בתים למצב הודעות, תכנון ...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח אתרי אינטרנט
כי ב-API של חברים ובשילוב עם סמארטפון, אימות JWT, הרשאה ברמת האובייקט והגבלת המאפיינים שניתן לעדכן מתורגמים ישירות לאבטחת מערכת הווב עצמה.
ייעוץ טכני וסקירת תכנון
כי איתור פערי הרשאה ב-API קיימים, הערכת רדיוס הפגיעה של ספריות תלויות וגיבוש כללי WAF זמניים בסקירת תכנון — כולם בתחום הייעוץ הטכני.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- למה תוקף יכול להתחזות למישהו אחר אף שאימות חתימת ה-JWT הצליח?
- בשאלה הזאת ספריית ניהול ה-JWT קיבלה את ה-alg בכותרת ה-JWT בדיוק כפי שהתוקף ציין אותו, וטיפלה ב-JWT עם alg=none כתקף בלי חתימה כלל. לכן גם כתיבה מחדש של מזהה המשתמש במטען עדיין עוברת אימות. תשובת הבחינה היא לאמת את ה-alg בכותרת ה-JWT ולוודא שערכו אינו NONE. בפועל, עם זאת, דחייה של NONE בלבד אינה מספיקה. קבעו את האלגוריתמים המותרים לשימוש — למשל RS256 — בתצורת צד השרת, כך שהאלגוריתם שהאסימון מכריז עליו לעולם לא ישמש ישירות לבחירה. יש גם לאמת את המנפיק, את הקהל, את תפוגת התוקף, את הנושא וכן הלאה, לפי השימוש.
- האם השוואת מזהה המשתמש שבתוך ה-JWT מול ה-mid של הבקשה מספיקה כהגנת הרשאה?
- לצורכי תשובת הבחינה הזאת, כן. אימות, בתוך המודול המשותף P, שמזהה המשתמש שב-JWT תואם ל-mid עוצר התקפה שמציינת mid של מישהו אחר. עם זאת, ל-API שמטפל רק במידע של הקורא עצמו, בטוח יותר בפועל לא לקבל mid מהלקוח כלל, ולקבוע את מזהה המשתמש מתוך ה-subject של ה-JWT שכבר אומת. שימוש ב-GET /users/me או PUT /users/me מקשה על כך שלוגיקת ההשוואה פשוט תיעדר. API שבו מנהל מפעיל משתמש אחר צריך להיות מופרד לנקודת קצה נפרדת עם מדיניות הרשאה משלו.
- למה אימות קלט רגיל לבדו אינו עוצר את ההתקפה שמוסיפה status?
- כי גם אם מאמתים את אורך name או את טווח age, זה לא עוזר אם status — שדה שלא היה אמור להתקבל מלכתחילה — נקשר אוטומטית ומועבר ישר לאובייקט הפנימי. הבעיה אינה פורמט הערך; זו הרשאה ברמת המאפיין, כלומר האם למשתמש מותר לשנות את המאפיין הזה בכלל. הגדירו רק name ו-age בסוג הקלט לעדכון, ודחו מאפיינים לא מוכרים. סטטוס החיוב חייב להשתנות רק מאירועים שהשרת סומך עליהם, למשל תוצאה מוצלחת משירות התשלום.
- קוד האימות בן ארבע הספרות פג אחרי 10 דקות — למה הוא עדיין מסוכן?
- כי יש רק 10,000 מועמדים מ-0000 עד 9999, וב-10 ניסיונות לשנייה תוקף מצליח אחרי 5,000 ניסיונות בממוצע — 500 שניות. תקופת התוקף של 10 דקות היא 600 שניות, כך שניסיון מועמדים בלי חזרות לפי הסדר מאפשר לתוקף לבדוק 6,000 מהם בחלון הזה. זמן התפוגה לבדו אינו יכול לעצור ניחוש כוחני. מרחב המועמדים, מהירות הניסיונות ותקרת מספר הניסיונות צריכים להיות מתוכננים יחד.
- ההגנה בבחינה היא נעילת חשבון — האם נעילה מיידית לבדה מספיקה גם בפועל?
- לא. הריק בשאלה דורש לוגיקה שנועלת את החשבון ברגע שמספר הכשלונות הרצופים חורג מסף, אבל נעילה קבועה וקבועה לבדה מאפשרת לתוקף לנעול במכוון חשבון של מישהו אחר כמניעת שירות. בפועל משלבים ספירת כשלונות לכל חשבון עם זמני המתנה מדורגים, הערכת סיכון של המקור והמכשיר, התראות והליך שחזור. חשוב גם שהנפקת קוד חדש לא תאפס את ספירת הכשלונות לאפס.
- מה הטעם להגדיר את ה-WAF לזיהוי ולא לחסימה?
- זה אומר שתעבורת עסק לגיטימית לא נעצרת גם כשמחרוזת רגילה נשפטת בטעות כהתקפה. בתשובת המודל, היתרון הוא שזה יכול למנוע חסימה שנגרמת מהתראת שווא, ומה שצריך לעשות הוא לבדוק אם זו התקפה בכל פעם שמתקבלת התראה. מצב זיהוי אינו הגדרה שמשאירים לבד. הוא משמש כתקופת תצפית שבה בודקים יומנים, מסננים התראות שווא, מכוונים את הכלל ואז עוברים לחסימה. בחירום שבו פגיעות קריטית ידועה מנוצלת בפועל, סביר לשקול זאת מול סיכון הזמינות ולבחור לחסום מההתחלה.
- האם ספרייה H בשאלה הזאת היא Log4j?
- השאלה מסתירה את שם המוצר, אבל רצף ההתקפה — JNDI Lookup, שרת LDAP, שליפת מחלקה משרת HTTP, מחרוזת המוטמעת בכותרת HTTP, וציון בסיס גבוה של CVSS v3.1 — נקרא באופן טבעי כהפשטה של CVE-2021-44228, הידועה כ-Log4Shell. המאמר מסביר את ההתאמה הזאת, אבל הבחינה אינה דורשת לנקוב בשם המוצר הספציפי. אפשר לענות רק מתוך הליך ההתקפה שניתן ומפרט ה-WAF.
- מה כדאי לקחת חזרה לפרקטיקה מהשאלה הזאת?
- שהצלחה באימות, שה-JWT לא שונה, שמותר לגשת לאובייקט היעד ושמותר לשנות את מאפיין היעד — אלה בדיקות נפרדות. מעבר לזה, קוד אימות קצר זקוק להגבלת קצב ניסיונות, ולפגיעות קריטית בספרייה מריצים במקביל אישור השפעה, הגנה זמנית ותיקון שורש. הלקחים המעשיים המרכזיים: לרכז הרשאה ברכיבים משותפים, להפוך את סכימת הקלט לרשימת היתר, לקבע את תנאי האימות של ה-JWT בצד השרת, ולעקוב אחרי ספריות תלויות כדי שניתן יהיה לעדכן אותן.