RISS אביב 2024, שאלת PM 1 — JWT alg=none, API authorization ו-WAF
· עודכן בתאריך: · Go Komura · מומחה אבטחת מידע רשום, מומחה אבטחה רשום, API, אבטחת API, JWT, authentication, authorization, WAF, Log4Shell, אבטחת מידע, פגיעות, IPA
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 6 Aug 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22175999)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). RISS אביב 2024, שאלת PM 1 — JWT alg=none, API authorization ו-WAF. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175999 https://comcomponent.com/he/blog/sc-exam-r6s-pm-q1-api-security/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22175999
- DOI (הגרסה הזו)
- 10.5281/zenodo.22176001
“מאמתים את חתימת ה-JWT, אז אפשר לסמוך על מזהה המשתמש.”
המשפט הזה נכון רק עד האמצע.
שאלת PM 1 בבחינת Registered Information Security Specialist (RISS) של IPA, אביב 2024, בנויה סביב API שנקרא מאפליקציית סמארטפון.1 אחרי authentication מוצלח מונפק JWT, וה-JWT מצורף לקריאות שמביאות ומעדכנות מידע משתמש. במבט ראשון זה מערך שגרתי.
באבחון עולות ארבע הבעיות הבאות.
- שינוי ה-
algבכותרת ה-JWT ל-noneנותן ל-JWT בלי חתימה לעבור - JWT תקף עם
midששונה למזהה של משתמש אחר מאפשר לקרוא או לעדכן מידע של מישהו אחר - הוספת
status=paidשלא מופיע ב-spec הופכת משתמש חינמי למשתמש משלם - קוד האימות בן 4 הספרות שנשלח במייל אפשר לנחש ב-brute-force בלי הגבלת ניסיונות
ארבעתן נראות כמו “פגיעויות סביב authentication”, אבל הסיבות שונות. מה שנשבר הם גבולות נפרדים: שלמות ה-token, authorization ברמת האובייקט, authorization ברמת property, והגבלת מספר ניסיונות.
בחצי השני מתווסף נושא אחר. מתפרסמת פגיעות קריטית בספריית open source נפוצה, שמאפשרת להריץ קוד מבחוץ דרך ניצול של JNDI Lookup. אין עדיין גרסה מתוקנת ואין כלל WAF מוכן. בינתיים השאלה שואלת איך לאשר את ההשפעה, איפה ה-WAF צריך לבדוק, ולמה המצב הראשון הוא detection ולא blocking.
המאמר נשען על תשובות המודל הרשמיות2 ועל הערות הבודקים3. הוא לא עוצר בתשובה לכל סעיף, אלא מסביר למה זו התשובה, ועד כמה להחמיר את התכנון בפרודקשן.
flowchart TB
accTitle: מפת השאלה כולה
accDescr: מראה איזה גבול אמון נשבר בכל שלב: קוד אימות, JWT, API authorization ופגיעות בספרייה
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. קודם כל, המסקנה
- התכונה של RESTful API שלא מחזיק session נקראת stateless. זה לא אומר שהשרת לא מחזיק בכלל database או מצב משתמש
- לקוד אימות בן 4 ספרות יש 10,000 ערכים. ב-10 ניסיונות לשנייה התוקף מצליח אחרי 5,000 ניסיונות בממוצע, כלומר 500 שניות. זה קצר מתוקף של 10 דקות, ולכן expiry לבדו לא עוצר את זה
- ההגנה המינימלית מול
alg=noneהיא לוודא שה-algבכותרת ה-JWT אינוNONE. בפרודקשן מקבעים בצד השרת את קבוצת האלגוריתמים המותרים - גם עם JWT תקף אסור לסמוך על ה-
midשל הבקשה. מתאימים את מזהה המשתמש שב-JWT מולmid, או יותר בטוח: לא מקבליםmidמהלקוח וקובעים את היעד מתוך ה-JWT - הוספת
status=paidהיא בעיית Mass Assignment: properties מחוץ ל-spec נכנסים לישות הפנימית. משתמשים ב-DTO של העדכון כ-allowlist, ולא נותנים למשתמש לשנות סטטוס חיוב - תשובת המודל ל-brute-force היא לוגיקה שנועלת את החשבון כשמספר הכשלונות הרצופים עובר סף. בפרודקשן מוסיפים גם backoff מדורג ובקרה לפי מקור
- כדי לאשר השפעה של פגיעות קריטית חדשה לא מריצים פקודה הרסנית. רושמים גישות ל-
index.htmlבשרת הבדיקה ומוודאים שהרצת קוד מבחוץ באמת מגיעה לשם - מחרוזת ההתקפה נמצאת בכותרת HTTP, לכן יעד הבדיקה של ה-WAF הוא
Header. כ-regex שמטפל בהחלפת רישיות משתמשים למשל ב-\W[jJ][nN][dD][iI]\W - היתרון בהתחלת ה-WAF במצב detection הוא שזה מונע חסימה של תעבורה לגיטימית בגלל false positive. כשמגיע alert בודקים אם זו התקפה, מכוונים את הכלל, ורק אז עוברים ל-blocking
- ה-WAF הוא mitigation זמני. תיקון השורש הוא עדכון הספרייה שנפגעה לגרסה מתוקנת
2. איך התרחיש ממופה לשאלות
התרחיש מתרחש בחברה G, שמשגרת שירות בריאות חדש. משתמשים מזינים ארוחות ומשקל דרך אפליקציית סמארטפון ומקבלים הערכת סיכון בריאותי וייעוץ תזונתי. המערכת בנויה בענן ומשלבת API gateway, עיבוד מונחה אירועים ו-database מנוהל.
השאלה מפשטת שמות מוצרים ושירותים. גם המאמר לא משחזר את הדיאגרמות או הטקסט של IPA, אלא מנסח מחדש רק את המבנה שדרוש להבנת השאלות.
| שאלה | נושא | פרק במאמר |
|---|---|---|
| שאלה 1 | אופי של RESTful API | פרק 4 |
| שאלה 2(1) | זמן brute-force של הקוד בן 4 הספרות | פרק 5 |
| שאלה 2(2) | JWT alg=none |
פרק 6 |
| שאלה 2(3) | גישה למשתמש אחר דרך mid |
פרק 7 |
| שאלה 2(4) | הקבלה של status מחוץ ל-spec |
פרק 8 |
| שאלה 2(5) | הגנה מפני brute-force | פרק 9 |
| שאלה 3(1) | אישור בטוח שהפגיעות קיימת | פרק 11 |
| שאלה 3(2)(3) | איפה ה-WAF בודק, וה-regex | פרק 12 |
| שאלה 3(4) | יתרון detection mode וההפעלה | פרק 13 |
הערות הבודקים אומרות ששיעור התשובות הנכונות הכללי היה בסביבות הממוצע. לעומת זאת, שיעור התשובות הנכונות היה נמוך מעט בהגנה מפני זיוף JWT בשאלה 2(2) ובמנגנון שנדרש בשרת הבדיקה בשאלה 3(1). אף אחת מהן לא נפתרת מאוצר מילים. צריך לעקוב איזה ערך התוקף שינה, לאיזה עיבוד הוא זרם, ואיפה בסוף סמכו עליו.
3. זו אינה “בעיית authentication” אחת
אם מסדרים את השאלה לפי גבול אמון, מקבלים את זה.
[מזהה משתמש / סיסמה]
|
v
[בדיקת קוד בן 4 ספרות] ---- אין הגבלת ניסיונות ----> brute-force
|
v
[הנפקת JWT]
|
v
[ספריית JWT] ------- מאפשרת alg=none ------> זיוף מזהה משתמש
|
v
[API משתמש]
| |
| +-- מעביר status כמו שהוא ----> פער authorization ברמת property
|
+-- סומך על mid -----------------------> פער authorization ברמת האובייקט
[רישום קלט חיצוני בלוג]
|
v
[ספרייה פגיעה] ---- JNDI/LDAP/HTTP ------> הרצת קוד מבחוץ
ההבחנה החשובה ביותר היא זו.
| בדיקה | מה היא שואלת | דוגמה שנשברה בשאלה |
|---|---|---|
| authentication | מי אתה | brute-force על הקוד בן 4 הספרות |
| אימות token | האם מידע הזהות זויף | alg=none |
| authorization ברמת האובייקט | האם מותר לגשת לנתונים של המשתמש הזה | החלפת mid |
| authorization ברמת property | האם מותר לשנות את השדה הזה | status=paid |
| גבול מקלט לביצוע | האם קלט חיצוני מתפרש כפקודה | JNDI Lookup |
הצלחה בבדיקה אחת אינה סיבה לדלג על הבאה. משתמש עם JWT תקף עדיין לא מורשה לקרוא נתונים של מישהו אחר. משתמש שמותר לו לעדכן את הפרופיל שלו עדיין לא מורשה לשנות גם את סטטוס החיוב.
ברגע שמפרידים את השלבים, התשובה לכל שאלה מפסיקה להיות שינון.
flowchart LR
accTitle: ההבדל בין authentication ל-authorization
accDescr: authentication מאשר את הסובייקט, authorization מאשר מה מותר לסובייקט הזה לעשות
auth[authentication<br/>מי אתה]
authz[authorization<br/>מה מותר לעשות]
auth --> authz
איור 2: authentication מול authorization. קודם מזהים מי זה, ואחר כך בודקים בנפרד מה מותר לו.
4. שאלה 1 — מה זה stateless
שאלה 1 שואלת על אחד מעקרונות התכנון של RESTful API: התכונה של אי-ניהול session.
התשובה היא stateless.
stateless אומר שהשרת לא צריך לזכור את מצב השיחה של הבקשה הקודמת, כי כל בקשה לבדה נושאת את כל מה שנחוץ לעיבוד. בשאלה הזאת אפליקציית הסמארטפון מצמידה JWT לכותרת Authorization בכל בקשה. השרת מאמת את ה-JWT ומזהה ממנו את המשתמש של אותה בקשה.
קריאה שגויה נפוצה היא לקחת “stateless” כ”השרת לא מחזיק מצב בכלל”. בפועל הוא בדרך כלל מחזיק את הדברים הבאים.
- database ששומר מידע משתמש ונתוני בריאות
- סטטוס חיוב
- ערך קוד האימות, תוקף וספירת כשלונות
- מפתח החתימה של ה-JWT
- מידע revocation, בתכנונים שמשתמשים ב-revocation list
- לוגים ורשומות audit
מה שהוא לא מחזיק הוא מצב session בצד השרת שקיים רק כדי להמשיך שיחה, כתנאי מוקדם שכל קריאת API תלויה בו.
להיות stateless גם לא משפר אוטומטית את האבטחה. שליחת JWT בכל בקשה מקלה על scale אופקי, אבל אם אימות ה-JWT שגוי, השגיאה מתפשטת באופן אחיד לכל צומת. תכונה ארכיטקטונית ונכונות אבטחה הם שני דברים שונים.
5. שאלה 2(1) — קוד בן 4 ספרות נפרץ בממוצע ב-500 שניות
ה-API ל-authentication שולח מספר בן 4 ספרות במייל ברגע שמזהה המשתמש והסיסמה תואמים. אחר כך הוא מנפיק JWT ברגע שמזהה המשתמש והקוד תואמים. הקוד תקף 10 דקות מרגע היצירה.
באבחון היו אפשריים 10 ניסיונות לשנייה. השאלה שואלת כמה שניות נדרשות בממוצע כדי לפרוץ.
החישוב הוא “חצי ממרחב הקודים”
מספר בן 4 ספרות, כולל אפס מוביל, כולל את 10,000 האפשרויות הבאות.
0000, 0001, 0002, ... , 9999
אם הקוד הנכון נבחר באקראי אחיד, תוקף שמנסה ערכים לפי הסדר בלי חזרות מגיע לנכון, בממוצע, אחרי חצי מהמרחב.
מספר ניסיונות ממוצע = 10,000 / 2 = 5,000
זמן ממוצע = 5,000 / 10 ניסיונות לשנייה = 500 שניות
לכן השדה הריק b הוא 500.
המקרה הגרוע ביותר נמשך עד 1,000 שניות, אבל השאלה שואלת על הממוצע. ותוקף הקוד הוא 600 שניות — ארוך מזמן הפריצה הממוצע של 500 שניות. לכן נשפט ש”סביר שייפרץ”.
flowchart LR
accTitle: למה 500 שניות מספיקות לקוד בן 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: למה 500 שניות מספיקות. בממוצע מנסים חצי מהמרחב, והקוד נפרץ בתוך התוקף.
לקצר רק את ה-expiry לא מספיק אם המרחב קטן
חוזק קוד האימות לא נקבע ממספר הספרות לבדו ולא מהתוקף לבדו.
מספר הניסיונות האפשריים במהלך התוקף
= ניסיונות לשנייה x תוקף
= 10 x 600
= 6,000 ניסיונות
ניסיון ערכים בלי חזרות לפי הסדר מאפשר לבדוק 60% מתוך 10,000 האפשרויות בתוך התוקף. קביעת זמן תפוגה אינה מספיקה אם מספר הניסיונות אינו מוגבל.
NIST SP 800-63B הנוכחי דורש לפחות שש ספרות לסודות קצרי טווח ב-out-of-band authentication, ומחייב הגבלת ניסיונות בכל פעם שלסוד יש פחות מ-64 סיביות entropy. הוא גם קורא לא להשתמש במייל ל-out-of-band authentication.4 תשובת הבחינה עובדת בתוך ה-spec הנתון של קוד בן 4 ספרות שנשלח במייל, אבל לתכנון חדש בפרודקשן כדאי לבחון מחדש גם את ההנחה עצמה.
6. שאלה 2(2) — alg=none נותן לתוקף לבחור איך מאמתים
ה-JWT בשאלה מורכב משלושה חלקים: header, payload וחתימה.
base64url(header).base64url(payload).base64url(signature)
בכותרת נרשם RS256 כאלגוריתם החתימה. ב-payload נמצאים מזהה המשתמש, זמן ההנפקה וה-expiry.
המאבחן שינה שני דברים.
- שינוי ה-
algבכותרת מ-RS256ל-NONE - שינוי מזהה המשתמש ב-payload למשתמש אחר
שליחת ה-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. סמוך על מזהה המשתמש ב-payload.
עצם חוזק האבטחה נבחר מקלט שהתוקף שולט בו.
תשובת הבחינה
השאלה שואלת, ב-20 תווים או פחות לכל אחד, אילו נתונים הספרייה Q המתוקנת צריכה לאמת, ומה האימות צריך לבדוק.
תשובת המודל היא כדלקמן.
| פריט | תמצית התשובה |
|---|---|
| נתונים לאימות | הערך שצוין ב-alg של כותרת ה-JWT |
| מה לאמת | שהוא אינו NONE |
כתיקון ישיר לפגיעות שתוארה בשאלה, זה נכון.
בפרודקשן אל תסתפקו ב”הכול מלבד NONE”
כאן צריך להפריד בין תשובת הבחינה לבין ההמלצה המעשית.
RFC 8725 קובע שספריית JWT צריכה לאפשר לקורא לציין קבוצת אלגוריתמים מותרים, וששום דבר מחוץ לקבוצה הזאת אינו רשאי לשמש.6 כלומר, הרעיון הוא זה.
גישה גרועה:
קבל אם token.header.alg != "none"
גישה טובה:
קבל רק אם זה כלול ב-serverConfig.allowedAlgorithms
לדוגמה allowedAlgorithms = ["RS256"]
דחייה של none בלבד עדיין יכולה להשאיר אלגוריתמים חלשים אחרים, או בלבול אלגוריתמים שבו סכימת מפתח ציבורי נחשבת בטעות לסכימת מפתח סימטרי. העיקרון הוא לא להוסיף תנאים שליליים למה לקבל, אלא לקבע קבוצה חיובית צרה של מה שמותר.
אימות JWT צריך לאשר לא רק את האלגוריתם אלא, לפי השימוש, לפחות גם את הבאים.
| פריט | מה לאשר |
|---|---|
| חתימה | האם אפשר לאמת במפתח ובאלגוריתם הצפויים |
iss |
האם זה issuer מהימן |
aud |
האם ה-token הונפק עבור ה-API הזה |
exp |
האם הוא בתוך התוקף |
nbf |
האם הוא אינו לפני זמן not-before |
sub או מזהה משתמש |
האם זה סובייקט תקף ביישום |
| סוג token | האם מבלבלים ID token עם access token וכו׳ |
בשאלה הזאת שם המפתח ב-payload הוא user, אבל בפרודקשן כדאי להשתמש ב-sub התקני או להגדיר בבירור את משמעות ה-claim המותאם.
flowchart TB
accTitle: אימות JWT בטוח מול לא בטוח
accDescr: אימות לא בטוח תלוי ב-alg, אימות בטוח משתמש ב-allowlist בצד השרת
subgraph "אימות לא בטוח"
D1[קרא alg מכותרת ה-JWT]
D2[קבל אם alg הוא none]
D1 --> D2
end
subgraph "אימות בטוח"
S1[אלגוריתמים מותרים בתצורת השרת<br/>למשל RS256]
S2[אשר ש-alg בכותרת ה-JWT<br/>נמצא ב-allowlist]
S3[אמת חתימה, iss, aud, exp]
S1 --> S2 --> S3
end
איור 4: אימות בטוח מול לא בטוח. בפרודקשן מקבעים קבוצה צרה של אלגוריתמים מותרים.
Base64url אינו הצפנה
יש אי-הבנה נפוצה נוספת לגבי JWT. הכותרת וה-payload מיוצגים ב-base64url, אבל זו אינה הצפנה. כל אחד יכול לפענח ולקרוא אותם.
מה שהחתימה מבטיחה, ורק כשהאימות מצליח, הוא שהתוכן לא שונה מאז ההנפקה. זה אינו אומר שמידע אישי שרוצים לשמור בסוד מותר לשים ב-payload של JWT חתום.
7. שאלה 2(3) — גם עם JWT תקף, שינוי mid קרא נתונים של מישהו אחר
הבא הוא התקפה שאינה מזייפת את ה-JWT עצמו.
ה-API למשתמש מקבל מזהה משתמש בשם mid ב-GET או PUT. המודול המשותף P מביא או מעדכן, ב-database, את מידע המשתמש הקשור ל-mid הזה.
מבנה ההתקפה פשוט.
מזהה משתמש בתוך ה-JWT: user01 <- JWT חתום כהלכה
ה-mid של הבקשה: user02 <- שונה בידי התוקף
חתימת ה-JWT תקפה, לכן authentication מצליח. אבל ה-API סומך על mid=user02 כפי שניתן, ומחזיר את המידע של user02.
זה מקרה ספר של מה ש-OWASP API Security Top 10 2023 מכנה Broken Object Level Authorization (BOLA). בכל פעם שניגשים לנתונים באמצעות מזהה אובייקט שהמשתמש ציין, חייבים לבדוק authorization לאובייקט הספציפי הזה בכל פעם.7
flowchart LR
accTitle: התקפת BOLA
accDescr: שימוש ב-JWT תקף תוך שינוי mid של הבקשה למזהה משתמש אחר
A[תוקף] -->|JWT: user01<br/>mid: user02| B[API משתמש]
B -->|סומך על mid| C[מחזיר נתוני user02 מה-database]
איור 5: התקפת BOLA. authentication עובר, אבל authorization לא נבדק.
תשובת השאלה
קו תחתון 2 בטבלה 5 שואל, ב-40 תווים או פחות, על העיבוד שצריך להוסיף לקריאה למודול המשותף P.
תשובת המודל היא:
לוגיקה שמוודאת אם מזהה המשתמש הכלול ב-JWT תואם לערך של mid
היתרון באימות בתוך המודול המשותף P הוא שאותה בדיקת authorization קלה ליישום גם ב-GET וגם ב-PUT, וגם ב-API עתידי שישתמש ב-P. העתקת אותה השוואה לכל מסך או endpoint בנפרד פירושה שבמקום כלשהו היא תיעדר.
תכנון בטוח יותר הוא לא לקבל 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
)
בדיקת השוואה מגנה עליכם אם כותבים אותה. אבל תכנון שלעולם אינו מקבל את מזהה היעד מבחוץ מצמצם את עצם קיום מחלקת הבאגים שבה שוכחים לכתוב את ההשוואה.
אם admin צריך להפעיל מידע של משתמש אחר, מפרידים כך.
PUT /users/me למשתמשים כלליים
PUT /admin/users/{userId} ל-admin
לנתיב ה-admin דורשים הרשאות נפרדות, רישום audit, ואם צריך — authentication מחדש. זה הופך את גבולות מדיניות ה-authorization לנראים יותר מאשר “הוסיפו חריג ל-API של המשתמש הכללי ל-admin בלבד”.
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[חיפוש ב-database לפי JWT sub]
איור 6: איך למנוע BOLA. לא מקבלים mid, או בודקים מול ה-subject של ה-JWT.
הבחנה בין authentication ל-authorization במשפט אחד
גם בבחינה וגם בפרודקשן, הניסוח הבא עוזר.
- authentication: מי אתה
- authorization: מה מותר לאותו אדם לעשות
הצלחה באימות חתימת JWT מגיעה רק עד “אפשר לסמוך על הסובייקט שה-token הזה מייצג”. האם “לאותו סובייקט מותר לקרוא את user02” חייבים לאשר בנפרד.
8. שאלה 2(4) — status=paid הוא פער authorization ברמת property
מפרט ה-API למשתמש מגדיר את הבאים כפרמטרי עדכון.
mid מזהה משתמש
name שם
age גיל
אולם המאבחן הוסיף את הערך הבא, שאינו ב-spec.
status=paid
סטטוס של משתמש בשכבת חינם השתנה אז לזה של משתמש משלם.
לפי השאלה, שירות L לא אימת את הפרמטרים שקיבל; הוא העביר את כולם ישר למודול המשותף P, שנבנה כך שיוכל לעדכן את ה-database ישירות.
התשובה לשדה הריק c היא המודול המשותף P.
flowchart TB
accTitle: Mass Assignment
accDescr: status=paid מחוץ ל-spec מתווסף ומוחל בשלמות על האובייקט הפנימי
A[מפרט API<br/>mid / name / age] -->|התוקף מוסיף status=paid| B[גוף הבקשה]
B -->|bind אוטומטי| C[מודול משותף P]
C -->|נשמר ב-database| D[סטטוס החיוב השתנה ל-paid]
איור 7: Mass Assignment. property מחוץ ל-spec מוחל בשלמות על האובייקט הפנימי.
ההבדל מ-BOLA
החלפת ה-mid בפרק הקודם והוספת ה-status הזאת נראות דומות, אבל רמת הפירוט המוגנת שונה.
| פגיעות | מה שהתוקף משנה | מה שצריך לבדוק בפועל |
|---|---|---|
החלפת mid |
אובייקט היעד | האם למשתמש הזה מותר לגשת לרשומת המשתמש הזאת |
הוספת status |
property בתוך האובייקט | האם למשתמש הזה מותר לשנות את השדה הזה |
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 ישירות. היעדר שדה מה-UI אינו גבול אבטחה.
מימוש בטוח הופך את השדות הניתנים לעדכון למפורשים.
input = parseExactSchema(body, fields = ["name", "age"])
entity = repository.find(authenticatedUserId)
entity.name = input.name
entity.age = input.age
repository.save(entity)
שני דברים חשובים כאן.
- סוג הקלט לעדכון צריך להחזיק רק את השדות שמותר למשתמש לשנות.
- במקום להתעלם בשקט משדות לא מוכרים מחוץ ל-spec, דחו אותם כשגיאה אם אפשר.
התעלמות שקטה משדות לא מוכרים מסתירה את העובדה שההתקפה נכשלה, אבל גם מאפשרת לפספס טעויות מימוש של הלקוח וסימני התקפה. אלא אם יש סיבת תאימות שלא לעשות זאת, דחייה בסכימה מחמירה מקלה על החקירה.
flowchart TB
accTitle: authorization ברמת property
accDescr: DTO העדכון מחזיק רק allowlist, ו-properties לא מוכרים נדחים
subgraph "DTO עדכון (allowlist)"
D1["name"]
D2["age"]
end
A[גוף הבקשה] -->|אימות סכימה| B{רק שדות<br/>מותרים קיימים}
B -->|כן| C[עדכן name/age של הישות]
B -->|לא| D[החזר שגיאה]
E[שירות תשלום<br/>התראה מאומתת] -->|נתיב ייעודי| F[עדכן status=paid]
איור 8: authorization ברמת property. מגבילים שדות לעדכון ב-allowlist, ומשנים סטטוס חיוב רק בנתיב נפרד.
status צריך להשתנות רק מתוצאת התשלום
status=paid אינו חלק מפרופיל המשתמש. זה מצב שנגזר מעובדה בצד השרת: שהתשלום הצליח.
עדכון פרופיל משתמש
-> רק name / age ניתנים לשינוי
התראה מאומתת משירות התשלום
-> התאם paymentId
-> מנע עיבוד כפול
-> שנה status ל-paid
גם כשנשמר באותה עמודת database, הסמכות לשנות אותו והנתיב שמשמש לשינוי הם דברים שונים. שימוש בישות פנימית ישירות כסוג קלט של API חיצוני מוחק את הגבול הזה.
9. שאלה 2(5) — הגנה מפני brute-force מחזיקה את ספירת הכשלונות כמצב
ל-brute-force של הקוד בן 4 הספרות, השדה הריק d בטבלה 5 שואל, ב-30 תווים או פחות, על העיבוד ששייך לשם. הסף הוא 10.
תשובת המודל היא:
לוגיקה שנועלת את החשבון ברגע שמספר הכשלונות הרצופים חורג מהסף
זה אינו סותר את ה-stateless משאלה 1. אי-החזקת מצב השיחה של קריאת API כ-session בשרת, ושימור ספירת הכשלונות הנחוצה להחלטת אבטחה, הם שני דברים שונים.
flowchart LR
accTitle: עם הגבלת ניסיונות ובלי
accDescr: בלי הגבלה הקוד נפרץ בממוצע ב-500 שניות, אבל הגבלת ספירת כשלונות מאטה בחדות את ההתקפה
subgraph "בלי הגבלה"
A1[10 ניסיונות לשנייה] -->|כ-500 שניות| B1[האימות מצליח]
end
subgraph "עם הגבלה"
A2[נעילה אחרי 10 כשלונות] -->|מהירות ההתקפה קורסת| B2[החשבון נעול]
C2[backoff מדורג] --> B2
end
איור 10: עם הגבלת ניסיונות ובלי. הגבלת ספירת כשלונות יכולה לעצור brute-force באופן מעשי.
בפרודקשן אל תסתמכו רק על נעילה קבועה
הגבלת ניסיונות לכל חשבון נחוצה, אבל אם תוקף יודע מזהה משתמש של מישהו אחר, הוא יכול להיכשל במכוון 10 פעמים כדי לנעול את המשתמש הלגיטימי. לכן בפרודקשן משלבים את הבאים.
| בקרה | תפקיד |
|---|---|
| ספירת כשלונות לכל חשבון | עוצרת brute-force מול חשבון יחיד |
| backoff מדורג | סובלים טעויות הקלדה של משתמש לגיטימי תוך האטת ההתקפה |
| בקרות לפי 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, מזהה המשתמש ב-payload |
אלגוריתם האימות שה-token עצמו מכריז עליו | קיבוע האלגוריתמים המותרים בצד השרת |
| קריאת מידע של משתמש אחר | ה-mid של הבקשה |
מזהה היעד שציין הלקוח | התאמה מול ה-subject של ה-JWT, או קביעת מזהה היעד מה-JWT |
| שדרוג למשתמש משלם | status מחוץ ל-spec |
כל ה-properties שנקשרים אוטומטית | הפיכת properties לעדכון ל-allowlist |
| פריצת הקוד בן 4 הספרות | מועמדים ל-otp |
ניסיונות authentication בלי הגבלה | הוספת הגבלות קצב, backoff והחלטות סיכון |
חשוב לא לאגד את הכול כ”validation של הקלט”.
algהוא מדיניות קריפטוגרפיתmidהוא authorization ברמת האובייקטstatusהוא authorization ברמת propertyotpהוא עמידות לניחוש אונליין
גם בתוך אותה בקשת HTTP, הסיבה שכל אחד מהם חייב להיות מוגן שונה.
11. שאלה 3(1) — לאשר הרצת קוד מבחוץ בלי לגרום נזק
אחרי השקת השירות מתפרסמת פגיעות קריטית V בספרייה H, ספריית open source נפוצה. רצף האירועים בשאלה הוא כדלקמן.
- התוקף שם מחרוזת שמכילה JNDI Lookup בכותרת HTTP ושולח אותה
- שרת היעד רושם את הערך הזה בלוג
- הספרייה הפגיעה מעריכה את ה-JNDI Lookup ושואלת את שרת ה-LDAP של התוקף
- תשובת ה-LDAP מחזירה את כתובת שרת ה-HTTP של התוקף
- שרת היעד שולף את קובץ ה-class ומריץ את הפקודה
כששם המוצר הספציפי מוסתר, זה נקרא כהתקפה מסוג Log4Shell (CVE-2021-44228). גם תיאור Apache עצמו מתאר את הפגיעות ככזאת שבה, אם תוקף שולט בהודעות לוג או בפרמטרים, הוא יכול להריץ קוד שרירותי שנטען משרת LDAP.9
flowchart LR
accTitle: זרימת אישור לפגיעות מסוג Log4Shell
accDescr: שימוש ב-callback בלתי מזיק כדי לאשר אם השרשרת מ-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/>access log]
F -->|אישור נגישות| G[הפגיעות אושרה]
איור 12: זרימת אישור לפגיעות מסוג Log4Shell. הנגישות מאושרת ברישום גישת HTTP ולא בהוצאת פקודה הרסנית.
קוד האימות מפעיל רק גישת HTTP בלתי מזיקה
חברה G מריצה קוד אימות שאין לו השפעה על המערכת, כדי לאשר אם אפשר לנצל את פגיעות V מבחוץ. הפקודה היחידה שקוד האימות מוציא היא שליפת index.html של שרת הבדיקה.
שאלה 3(1) שואלת מה צריך לממש בשרת הבדיקה כדי לאשר שהפקודה הורצה.
תשובת המודל היא:
מנגנון שרושם ומאפשר לאשר גישות ל-index.html של שרת הבדיקה
אם ה-access log של שרת ה-Web רושם GET משרת היעד, זה מאשר לפחות שהשרשרת הבאה עברה.
בקשת HTTP חיצונית
-> עיבוד לוג
-> JNDI Lookup
-> תשובת LDAP
-> שליפת class
-> הרצת פקודת אימות
-> גישת HTTP לשרת הבדיקה
למה “הצגת טקסט על המסך” אינה מספיקה
יעד ההתקפה הוא השרת. אין ערובה שמשהו משתנה במסך הדפדפן של המשתמש. גם במקום שבו הפגיעות קיימת, התקשורת היוצאת באמצע הדרך יכולה להיחסם ב-firewall.
רישום הגישה בצד שרת הבדיקה מייצר ראיה שניתן לצפות בה ששרת היעד באמת הגיע החוצה.
כשמבצעים אימות מהסוג הזה בפרודקשן, תמיד שומרים על הבאים.
- קבלו הרשאה מפורשת מבעל מערכת היעד
- השתמשו בשיטת אימות שאין לה השפעה על הייצור, או שהשפעתה קטנה ומקובלת
- אל תשתמשו בפקודות הרסניות כמו כתיבה, מחיקה או שינויי תצורה
- נהלו בעצמכם את דומיין האימות ואת השרת
- רשמו את זמן האימות, המקור, היעד וה-callback הצפוי
- פרקו שרתי LDAP או HTTP זמניים ו-credentials אחרי האימות
“לאשר שאפשר להריץ קוד שרירותי” ו”להריץ קוד מסוכן שרירותי” אינם אותו דבר. שמרו את תופעות הלוואי במינימום שנחוץ לעמידה במטרה.
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
בחוברת השאלות הלוכסן האחורי יכול להיראות כמו סימן ין בגליף של הסביבה היפנית, אבל כ-regex זה \W. \W מתאים לכל תו שאינו אות, ספרה או קו תחתון. בתחביר JNDI Lookup מופיעים תווים שאינם מילה כמו ${ ו-: מיד לפני ואחרי jndi, והתבנית כתובה כדי לתפוס גם אותם.
אפשר ליישם את אותו רעיון כדי להפוך גם את צד ה-ldap לבלתי רגיש לרישיות.
\W[lL][dD][aA][pP]\W
אל תתייחסו ל-regex הזה כאל “הגנת Log4Shell שלמה”
הבחינה מבקשת regex שמטפל בטכניקת ההתחמקות שמוצגת בטקסט השאלה. התקפות בעולם האמיתי יכולות לכלול פיצול מחרוזות, Lookup חלופי, encoding, פרוטוקולים אחרים ווריאציות אחרות שקשה לכסות בחתימות בלבד.
לכן המיקום המעשי הוא כדלקמן.
- חסמו באופן זמני דפוסי התקפה ידועים כעת עם ה-WAF
- בדקו אם הספרייה שנפגעה באמת קיימת
- הגבילו תעבורה יוצאת של LDAP, RMI ו-HTTP מיותר
- עדכנו לגרסה מתוקנת
- אחרי העדכון, עדיין בדקו את הלוגים וחקרו אם אירעה פריצה
ה-WAF הוא שכבה שקונה זמן עד שגרסה מתוקנת זמינה.
flowchart TB
accTitle: מקומו של ה-WAF
accDescr: ה-WAF הוא שכבת mitigation זמנית, תיקון השורש הוא עדכון הספרייה לגרסה מתוקנת
A[פורסמה פגיעות קריטית] --> B[אישור השפעה]
B --> C[mitigation זמני]
C -->|כלל WAF<br/>detection/blocking| D[עצירת דפוס ההתקפה באופן זמני]
C -->|הגבלת תעבורה יוצאת| E[סגירת נתיב הניצול]
D --> F[עדכון לספרייה המתוקנת]
E --> F
F --> G[סקירה לאחר מעשה ומניעה]
איור 13: מקומו של ה-WAF. ה-WAF רק קונה זמן עד שמגיע תיקון; תיקון השורש הוא העדכון.
13. שאלה 3(4) — למה להתחיל ב-detection
לגבי כלל ה-WAF המעודכן, Z, מומחה אבטחה רשום, מייעץ להגדיר את המצב ל-detection ולא ל-blocking לתקופה קבועה אחרי שהוא עולה לייצור.
השאלה שואלת, ב-25 תווים או פחות לכל אחד, על יתרון השימוש במצב detection ועל מה שצריך לעשות כדי למזער נזק.
תשובת המודל היא:
| פריט | תמצית התשובה |
|---|---|
| יתרון | יכול למנוע חסימה שנגרמת מ-false positive |
| מה לעשות | לבדוק אם זו התקפה בכל פעם שמתקבל alert |
detection mode אינו מצב “אל תעשה כלום”
במצב detection, תעבורה שתואמת לכלל עדיין עוברת, אבל היא נרשמת ומופעל alert. גם אם המחרוזת jndi או ldap מופיעה במקרה בתוך קריאת API לגיטימית, העסק אינו נעצר מיד.
בתמורה, צד ההפעלה צריך לעשות את הבא.
התקבל alert
|
v
בדוק את הבקשה הנדונה
|
+-- תעבורה לגיטימית -> צמצם את הכלל, שקול חריג
|
+-- התקפה -> בודד את היעד, שמר לוגים, חקור השפעה, עבור ל-blocking
אם אף אחד לא מביט ב-alerts, למצב detection אין כלל אפקט הגנתי. detection עובד רק בצמד עם תהליך הפעלה שצופה ושופט.
הנתיב מ-detection ל-blocking
הליך השקה טיפוסי הוא כדלקמן.
- הריצו detection mode מול תעבורה אמיתית
- סווגו פגיעות כ-false positives או כחיוביות אמיתיות
- כוונו את כותרת היעד, הנתיב, ה-API, גבולות מילים וכן הלאה
- אשרו שההשפעה על תעבורה לגיטימית מקובלת
- עברו למצב blocking
- עקבו אחרי מספר החסימות וההשפעה העסקית
זה, עם זאת, העיקרון לזמנים רגילים. כשפגיעות קריטית, מנוצלת בפועל, ואין חלופה, אפשר לשפוט שההשבתה שנגרמת מפריצה עולה על זו שנגרמת מ-false positive, ולבחור ב-blocking מההתחלה. בתרחיש הבחינה בוחרים קודם במצב detection כדי לאשר שהשירות יכול להמשיך לפעול כמו קודם.
flowchart LR
accTitle: ממצב detection של WAF למצב blocking
accDescr: צופים ב-alerts במצב detection, מכוונים false positives, ואז עוברים למצב blocking
A[מצב detection] -->|תעבורה אמיתית| B[הופעל alert]
B --> C{התקפה או<br/>false positive}
C -->|false positive| D[כוונון הכלל]
D --> A
C -->|התקפה| E[מעבר למצב blocking]
E --> F[מעקב אחרי מספר חסימות והשפעה עסקית]
איור 14: מ-detection ל-blocking. קודם צופים ומכוונים, מאשרים שההשפעה מקובלת, ואז עוברים לחסימה.
14. ה-WAF הוא אמצעי זמני; העדכון הוא תיקון השורש
בשאלה, באתר הרשמי של ספרייה H לא היה עדיין תיקון וגם לא פתרון זמני, ואפילו כלל ה-WAF המקיף של ספק הענן היה אמור לקחת עד 72 שעות. לכן חברה G מאשרת את ההשפעה בעצמה וחוסמת באופן זמני לפחות את הדפוסים שכבר זוהו.
הרצף הזה הוא הצורה הבסיסית של תגובה לאירוע.
| שלב | מטרה | התגובה בשאלה זו |
|---|---|---|
| אישור השפעה | לשפוט אם הארגון עצמו באמת בסיכון | אישור ניצול מבחוץ ב-callback בלתי מזיק |
| mitigation זמני | לקנות זמן עד שיגיע תיקון | כללי WAF, detection/blocking, הגבלות תעבורה יוצאת |
| תיקון שורש | להסיר את הסיבה הפגיעה | עדכון לספרייה מתוקנת |
| סקירה לאחר מעשה | לבדוק אם כבר נוצלה | חקירת לוגי WAF, יישום, DNS, proxy ואחרים |
| מניעת הישנות | להאיץ את ההחלטה הבאה | מלאי dependencies, SBOM, הליך עדכון, נתיב קשר |
“אנחנו לא יודעים אם אנחנו משתמשים בזה” הוא מקור העיכוב הגדול ביותר
בשאלה, גם כשחברה G שואלת את חברה F אם היא משתמשת בספרייה H, התשובה לוקחת זמן כי נדרש ניתוח תצורה מפורט.
בפרודקשן, אם מתחילים לחפש קובצי JAR רק אחרי שפורסמה פגיעות קריטית, התגובה מתעכבת. כדאי שיהיו לפחות הבאים במצב שגרה.
- מלאי dependencies ישירות וטרנזיטיביות
- הרכיבים והגרסאות שכלולים בפועל ב-artifacts
- לאילו שירותים, containers ומכשירים הם פרוסים
- הליך לעדכון ספרייה תלויה ולבנייה מחדש/הפצה מחדש
- נתיב קשר לאישור שינויי חירום
- היעדים המותרים לתעבורה יוצאת, וההשפעה של חסימתם
- היכן נשמרים לוגים ואיך לחפש בהם
SBOM אינו המטרה עצמה. הוא אינדקס כדי לענות, בזמן קצר, “אילו מערכות רצות מושפעות מהפגיעות הזאת”.
אל תפסיקו לחקור ברגע שעדכנתם
ייתכן שכבר הותקפתם בסביבות זמן פרסום הפגיעות. עדכון לגרסה מתוקנת עוצר ניצול עתידי, אבל אינו מוחק credentials שכבר נפרצו או backdoor שכבר ניטעה.
לפגיעות מסוג Log4Shell, חוקרים לפחות את הזוויות הבאות.
- בקשות HTTP שמכילות מחרוזות חשודות המצביעות על JNDI או LDAP
- תקשורת משרת היישום החוצה ל-LDAP, RMI או HTTP חיצוניים
- השקת תהליכי בן חריגים
- יצירת JAR, classes, סקריפטים או קבצים להרצה חשודים
- גישה ל-credentials בענן או למשתני סביבה
- authentication, שינויי הרשאות והעברות יוצאות בסביבות זמן העדכון
חשוב לא להסיק “לא הותקפנו” מלוגי WAF בלבד. יש נתיבים פנימיים שלעולם אינם עוברים ב-WAF, ולוגים שלא נשמרו בעבר.
15. דרך קריאה שמקלה להרוויח נקודות בבחינה
השאלה הזאת פחות מבחן ידע ויותר תרגיל בקריאת הפער בין spec למימוש.
15.1 הפרידו “spec” מ”מימוש” בטבלאות
בסוגיית ה-status, ערך שנעדר ממפרט ה-API עובר במימוש.
spec:
mid / name / age
מימוש:
שלח את כל הפרמטרים שהתקבלו ל-P
ברגע שרואים את הפער הזה, מתברר שהשדה הריק c הוא המודול המשותף P.
15.2 סמנו קו מתחת לערך שהתוקף שינה
הערך ששונה בכל התקפה הוא כדלקמן.
- ה-
algבכותרת ה-JWT - מזהה המשתמש ב-payload של ה-JWT
- פרמטר ה-API
mid statusמחוץ ל-spec- ה-
otpשל ה-API ל-authentication - כותרת ה-HTTP
x-api-version
כמעט כל שאלה שואלת “היכן צריך לאמת את הערך הזה”.
15.3 החזירו את התשובה למונחי טקסט השאלה עצמו
בפרודקשן אפשר לקרוא לאלה “BOLA”, “Mass Assignment” ו-“rate limiting”. אבל מה שהשאלה מבקשת הוא עיבוד קונקרטי שמותאם למבנה טקסט השאלה.
דוגמה גרועה:
בצעו authorization כראוי.
דוגמה טובה:
ודאו אם מזהה המשתמש הכלול ב-JWT תואם לערך של mid.
דוגמה גרועה:
נקטו הגנות מפני brute-force.
דוגמה טובה:
נעלו את החשבון ברגע שמספר הכשלונות הרצופים חורג מהסף.
ידיעת השם המופשט לבדה אינה מייצרת תשובה שניתן לנקוד בתוך מגבלת התווים.
15.4 ב-WAF, עקבו אחרי “היכן זה הונח”
יעד הבדיקה של ה-WAF נקבע לא בניחוש מסוג ההתקפה, אלא מהמיקום שבו הונחה מחרוזת ההתקפה.
הונח בכותרת x-api-version
↓
יעד הבדיקה הוא Header
הערה בהערות הבודקים ששיעור התשובות הנכונות בשאלה 3(1) היה נמוך במקצת נובעת גם מכך שהרבה תשובות לא תאמו לזרימת ההתקפה באיור 6. עצם ציור מחדש של רצף ההתקפה עם חצים כבר חושף מה צריך לצפות.
16. רשימת בדיקה לסקירות API בעולם האמיתי
רשימת בדיקה להחזרת השאלה הזאת לתכנון ולסקירות קוד בפועל.
אימות JWT
- אלגוריתם החתימה המותר מקובע בתצורת השרת
noneואלגוריתמים בלתי צפויים נדחים- חתימה,
iss,aud,expו-nbfמאומתים לפי השימוש - ID tokens, access tokens ו-refresh tokens אינם מתבלבלים זה בזה
- יש הליך לסיבוב מפתחות ול-revocation
- מידע שחייב להישאר חסוי אינו מוכנס ל-payload של ה-JWT
authorization ברמת האובייקט
- שינוי המזהה בתוך בקשה אינו יכול להגיע לנתוני משתמש אחר
- ה-authorization נאכף ברשימה, בפירוט, בעדכון, במחיקה ובהורדה כאחד
- ה-authorization נאכף בשכבה משותפת שמגיעה לנתונים, לא במסך
- ל-API ליחיד בלבד, שקלתם לגזור את מזהה היעד מה-token במקום
- פעולות admin משתמשות במדיניות נפרדת מ-API המשתמש הכללי
authorization ברמת property
- סוג הקלט החיצוני וישות ה-database נשמרים נפרדים
- שדות לעדכון מנויים כ-allowlist
- properties מחוץ ל-spec נדחים או מבוקרים
- מצב כמו הרשאות, חיוב, אישור ובעלות אינו ניתן לשינוי מקלט משתמש
- גם התשובות מדירות properties חסויים מיותרים
ניסיונות authentication
- יש הגבלת ספירת כשלונות לכל חשבון
- יש backoff מדורג ובקרה לפי מקור
- ספירת הכשלונות אינה מתאפסת כשהקוד מונפק מחדש
- קוד האימות ניתן לשימוש פעם אחת בלבד
- קודי אימות וסיסמאות אינם נשארים בלוגים
- הליך השחרור/השחזור אינו עצמו נתיב authentication חלש יותר
פגיעויות קריטיות בספריות תלויות
- אפשר למפות שירותים רצים לגרסאות ה-dependencies שלהם
- יש הליך לאימות השפעה בשיטה בלתי מזיקה
- אפשר ליישם אמצעים זמניים כמו כללי WAF והגבלות יוצאות
- יש תהליך הפעלה שבו אדם אחראי סוקר alerts של detection
- יש נתיב שחרור חירום לעדכון לגרסה מתוקנת
- בודקים לוגים לאפשרות ניצול לפני העדכון
17. שני הפנים של רכיבים משותפים, כפי שנראים דרך השאלה הזאת
בשאלה מופיעים שני רכיבים משותפים: ספריית ניהול JWT Q והמודול המשותף P.
לרכיבים משותפים יש יתרונות גדולים.
- תיקון במקום אחד מתפשט לכל API שמשתמש בו
- לוגיקת authorization ואימות אינה חייבת להיות משוכפלת לכל יכולת
- אפשר לרכז יעדי בדיקה
- אפשר לאחד פורמטי לוג ו-audit
מצד שני, גם טעויות מתפשטות בכל המערכת.
- אם ספרייה 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 היא שאלה שקוראת ומפרידה את נושאי אבטחת ה-API אחד-אחד.
שימוש ב-JWT אינו אומר שה-authentication בטוח. מתן בחירת אלגוריתם החתימה לתוקף מאפשר לכתוב מחדש את מזהה המשתמש.
חתימת JWT תקפה אינה אומרת שה-authorization נכון. אמון ב-mid של הבקשה מאפשר למשתמש לגיטימי לגשת למידע של מישהו אחר.
היכולת לעדכן את האובייקט שלך אינה אומרת שמותר לשנות כל property. bind אוטומטי של מצב פנימי כמו status מאפשר לכתוב מחדש הרשאות או סטטוס חיוב.
קיום תוקף לקוד האימות אינו אומר שהוא עמיד ל-brute-force. צריך לחשב את מרחב הקודים ואת מהירות הניסיונות, ולהגביל את מספר הכשלונות.
הכנסת כלל ל-WAF אינה אומרת שהפגיעות תוקנה. detection ו-blocking רק קונים זמן; מאשרים את ההשפעה ובסופו של דבר מעדכנים את הספרייה.
flowchart TB
accTitle: מיפוי פגיעויות להגנות
accDescr: ממפה כל פגיעות לגבול האמון שלה ולהגנה עליה
A[זיוף JWT] -->|אימות token| B[קיבוע האלגוריתם המותר]
C[החלפת mid] -->|authorization ברמת האובייקט| D[התאמה מול subject של JWT / אין צורך ב-mid]
E[status=paid] -->|authorization ברמת property| F[הפיכת DTO העדכון ל-allowlist]
G[brute-force של קוד בן 4 ספרות] -->|בקרת ניסיונות authentication| H[הגבלת כשלונות / backoff]
I[פגיעות מסוג Log4Shell] -->|מקלט לביצוע| J[עדכון ספרייה / WAF]
איור 15: מיפוי פגיעויות להגנות. מפרידים את התיקון לפי הגבול שנשבר.
עיקרון אחד עובר בכל השאלה הזאת.
לעולם אל תהפכו הצלחה בבדיקה הקודמת לסיבה לדלג על גבול האמון הבא.
מאמרים קודמים בסדרה מכסים את stored XSS בשאלת PM 1 בסתיו 2023 ואת הוצאת נתונים מ-Wi-Fi לאורחים בשאלת PM 2 בסתיו 2023. למבט על מה לבדוק באתר כולו, ראו גם שימוש ב-“How to Secure Your Website” של IPA כרשימת בדיקה.
flowchart TB
accTitle: סיכום סופי
accDescr: מראה שהצלחה בבדיקה הקודמת לעולם אינה סיבה לדלג על גבול האמון הבא
A[ה-authentication מצליח] --> B[אימות חתימת JWT]
B --> C[authorization ברמת האובייקט]
C --> D[authorization ברמת property]
D --> E[הגבלת ניסיונות]
E --> F[גבול מקלט לביצוע]
F --> G[WAF / עדכון ספרייה]
איור 16: סיכום סופי. גבולות אמון נבדקים בשלבים, ואסור לדלג על אף אחד.
מקורות
-
IPA, 令和6年度 春期 情報処理安全確保支援士試験 午後 問題冊子. טקסט השאלה שעליו מבוסס המאמר. ↩
-
IPA, 令和6年度 春期 情報処理安全確保支援士試験 午後 解答例. תשובת המודל הרשמית לכל שאלה. ↩
-
IPA, 令和6年度 春期 情報処理安全確保支援士試験 午後 採点講評. הסבר של שיעורי תשובות נכונות וטעויות נפוצות. ↩
-
NIST, SP 800-63B: Authentication and Authenticator Management. קובע מספרי ספרות לסודות קצרי טווח, הגבלות ניסיונות, ספירת הכשלונות בהנפקה מחדש, ואי-שימוש במייל ל-out-of-band authentication, בין שאר הדרישות. ↩ ↩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 שקובע קיבוע קבוצת האלגוריתמים המותרים, ואימות issuer, subject ו-audience, בין שאר הפרקטיקות. ↩
-
OWASP, API1:2023 Broken Object Level Authorization. מסביר את הצורך לבדוק authorization לכל מזהה אובייקט שהמשתמש מציין. ↩
-
OWASP, API3:2023 Broken Object Property Level Authorization. מסביר פערים ב-authorization ברמת property, כולל Mass Assignment, ואת ההגנות עליהם. ↩
-
Apache Logging Services, Security. מסביר את השפעת CVE-2021-44228, הרצת קוד דרך JNDI ו-LDAP, והגרסאות המתוקנות. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
בחינת IPA SC, סתיו 2023 (Reiwa 5), שאלה 1 אחר-הצהריים — ה-Stored XSS שבו 16 ביקורות מוצגות כ-2
ניתוח של שאלת אחר-הצהריים 1 בבחינת IPA SC בסתיו 2023 (Reiwa 5): איך הגבלת מספר התווים נשברה בפרסום מפוצל, איך ה-session ID הוצא החוצה כתמ...
IPA SC, סתיו 2023, שאלה 2 אחר-הצהריים — קבצים שיוצאים מ-Wi-Fi לאורחים
על בסיס שאלת אחר-הצהריים 2 בסתיו 2023 של בחינת IPA SC (Registered Information Security Specialist), המאמר מסביר איך חברה שחסמה USB עדיין ...
למה passkey בטוח — אימות בלי לשלוח את הסוד
למה passkey בטוח, עם תרשימים. Public-key cryptography שמשאירה את ה-private key מחוץ לשרת ומחוץ לרשת, למה phishing נכשל ברמת הפרוטוקול, מה...
לבנות כלי ניתוח ל-PowerShell בתוך PowerShell — לקרוא סקריפטים עם AST, לא עם ביטויים רגולריים
להציג AST אמיתי של PowerShell ולראות לאיזה אובייקט הופכת כל שורת קוד. לעבור מ-ScriptBlockAst ל-CommandAst ולמצוא היכן נקרא Write-Host, בל...
איך קיצור דרך ב-Windows מוצא קובץ שהעברתם? — המיקום של הקובץ והזהות שלו הם שני דברים שונים
העברתם את הקובץ המקורי, ובכל זאת הקיצור פותח אותו. Windows מחפש את היעד לא רק לפי הנתיב השמור, אלא גם לפי מזהי מעקב ומאפיינים של הקובץ, ו...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח אתרי אינטרנט
ב-API של חברים ובחיבור מאפליקציית סמארטפון, אימות JWT, authorization ברמת האובייקט והגבלת השדות שמותר לעדכן קובעים ישירות את האבטחה של מערכת ה-Web.
ייעוץ טכני וסקירת תכנון
סקירת תכנון שמאתרת פערי authorization ב-API קיים, בודקת איזה dependency באמת נפגע, וכותבת כלל WAF זמני נמצאת בתחום הייעוץ הטכני.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- למה אפשר להתחזות למשתמש אחר גם אחרי שאימות חתימת ה-JWT הצליח?
- בשאלה הזאת ספריית JWT קיבלה את ה-alg בכותרת בדיוק כמו שהתוקף שלח אותו, וטיפלה ב-JWT עם alg=none כתקף בלי חתימה. לכן גם אחרי שמשנים את מזהה המשתמש ב-payload האימות עדיין עובר. תשובת הבחינה: בודקים את ה-alg בכותרת ה-JWT ומוודאים שהוא לא NONE. בפרודקשן זה לא מספיק. מקבעים בצד השרת את האלגוריתמים המותרים, למשל RS256, ולא נותנים ל-token לבחור את שיטת האימות. בנוסף מאמתים issuer, audience, expiry ו-subject לפי השימוש.
- האם להשוות את מזהה המשתמש שב-JWT מול mid של הבקשה זה מספיק כ-authorization?
- לתשובת הבחינה כן. במודול המשותף P, בדיקה שמזהה המשתמש ב-JWT תואם ל-mid עוצרת התקפה שמציינת mid של מישהו אחר. ל-API שמטפל רק בפרופיל של הקורא עצמו עדיף בכלל לא לקבל mid מהלקוח, ולקחת את המזהה מ-subject של JWT שכבר אומת. GET /users/me או PUT /users/me מקשים על באג שבו שכחו לכתוב את ההשוואה. API שבו admin מפעיל משתמש אחר שייך ל-endpoint נפרד עם מדיניות authorization משלו.
- למה validation רגיל של קלט לא עוצר את ההתקפה שמוסיפה status?
- כי בדיקת אורך של name או טווח של age לא עוזרת אם status, שדה שלא היה אמור להגיע מהלקוח, נכנס ב-bind אוטומטי לישות הפנימית. זו לא בעיית פורמט. זו authorization ברמת property: האם למשתמש מותר לשנות את השדה הזה. ב-DTO של העדכון מגדירים רק name ו-age ודוחים properties לא מוכרים. סטטוס החיוב משתנה רק מאירוע שהשרת סומך עליו, למשל הצלחה משירות התשלום.
- קוד האימות בן 4 הספרות פג אחרי 10 דקות. למה הוא עדיין מסוכן?
- כי מ-0000 עד 9999 יש רק 10,000 אפשרויות. ב-10 ניסיונות לשנייה התוקף מצליח אחרי 5,000 ניסיונות בממוצע, כלומר 500 שניות. תוקף של 10 דקות הוא 600 שניות, כך שניסיון בלי חזרות לפי הסדר מכסה 6,000 ערכים בחלון. expiry לבדו לא עוצר brute-force. מתכננים יחד את גודל מרחב הקודים, קצב הניסיונות ואת תקרת מספר הניסיונות.
- ההגנה בבחינה היא account lockout. האם נעילה מיידית לבדה מספיקה גם בפרודקשן?
- לא. בשדה הריק נכנסת לוגיקה שנועלת את החשבון כשמספר הכשלונות הרצופים עובר סף, אבל נעילה קבועה לבדה נותנת לתוקף לנעול בכוונה חשבון של מישהו אחר כ-DoS. בפרודקשן משלבים ספירת כשלונות לכל חשבון, backoff מדורג, הערכת סיכון לפי מקור ומכשיר, התראות והליך שחזור. חשוב גם שהנפקת קוד חדש לא מאפסת את ספירת הכשלונות.
- מה הרווח בהפעלת ה-WAF במצב detection ולא blocking?
- תעבורה לגיטימית ממשיכה גם כשמחרוזת תמימה נשפטת בטעות כהתקפה. בתשובת המודל היתרון הוא מניעת חסימה בגלל false positive, ומה שצריך לעשות הוא לבדוק אם זו באמת התקפה בכל alert. detection mode אינו מצב שמניחים בצד. זו תקופת תצפית: בודקים לוגים, מסננים false positives, מכוונים את הכלל ואז עוברים ל-blocking. בחירום שבו פגיעות קריטית כבר מנוצלת, אפשר לשקול מול סיכון הזמינות ולחסום מההתחלה.
- האם ספרייה H בשאלה היא Log4j?
- השאלה מסתירה את שם המוצר, אבל שרשרת ההתקפה — JNDI Lookup, שרת LDAP, שליפת class משרת HTTP, מחרוזת בכותרת HTTP, וציון CVSS v3.1 גבוה — נקראת באופן טבעי כהפשטה של CVE-2021-44228, כלומר Log4Shell. המאמר מסביר את ההתאמה, אבל בבחינה אין צורך לנקוב בשם המוצר. אפשר לענות מתוך שלבי ההתקפה שניתנו ומפרט ה-WAF.
- מה כדאי לקחת מהשאלה הזאת חזרה לעבודה?
- הצלחה ב-authentication, שה-JWT לא שונה, שמותר לגשת לאובייקט היעד ושמותר לשנות את ה-property — אלה בדיקות נפרדות. בנוסף, קוד אימות קצר דורש הגבלת ניסיונות, ולפגיעות קריטית בספרייה רצים במקביל על אישור השפעה, הגנה זמנית ותיקון שורש. בפועל: מרכזים authorization ברכיבים משותפים, הופכים את סכימת הקלט ל-allowlist, מקבעים את תנאי האימות של ה-JWT בצד השרת, ומחזיקים מלאי dependencies כדי שאפשר יהיה לעדכן אותן.