אבטחת Web ו-API
קריאת JavaScript בעזרת AI כדי למצוא endpoints
ה-JavaScript בצד הלקוח מפרט את ה-API שהאפליקציה מדברת איתו. בעזרת מודל AI קוראים bundles ומעלים endpoints ופרמטרים, ואז מאמתים כל אחד בעצמך.
XSS מסוג reflected, stored ו-DOM-based, למה קידוד פלט תלוי בהקשר, ואיך CSP מוסיף שכבת הגנה, במעבדה מקומית.
Cross-site scripting (XSS) קורה כשאפליקציית web לוקחת נתונים שתוקף יכול להשפיע עליהם ומכניסה אותם לדף בצורה שהדפדפן קורא כקוד. הדפדפן מריץ את הקוד הזה עם ההרשאות של ה-origin של הדף. הקוד יכול לקרוא את הדף, לשלוח בקשות בשם המשתמש ולשנות את מה שהמשתמש רואה.
הבעיה היא ערבוב של נתונים וקוד באפליקציה, והדפדפן לא יכול לדעת איזה חלק הגיע מהמפתח ואיזה ממשתמש. העובדה שסקריפט מגיע מאתר אחר היא לא העיקר. MITRE מסווג את זה כ-CWE-79, "Improper Neutralization of Input During Web Page Generation." הניסוח חשוב: הפגם הוא כשל בנטרול קלט בנקודה שבה הדף נבנה.
XSS היא אחת מחולשות ה-web הנפוצות ביותר, והיא נשארת נפוצה כי היא מסתתרת ביכולות רגילות: תיבת חיפוש שמחזירה את השאילתה, שדה תגובה, שם פרופיל, fragment של URL שאפליקציית single-page קוראת בטעינה. תקיפה מוצלחת רצה בתוך הסשן המחובר של הקורבן. היא יכולה לקרוא כל מה שהדף יכול לקרוא, להפעיל פעולות שמותר למשתמש לבצע או להציג טופס התחברות מזויף. דפדפנים מגבילים חלק מזה דרך same-origin policy, אבל XSS עובדת בדיוק כי היא רצה בתוך ה-origin שהמשתמש כבר סומך עליו.
עבור מפתח, הלקח המעשי צר ושימושי. לא צריך לסנן כל מחרוזת רעה. צריך לוודא שטקסט לא מהימן לעולם לא יפורסר בדפדפן כ-markup או כקוד.
OWASP מתאר שלוש משפחות. ההבדל ביניהן הוא לאן הנתונים הלא מהימנים עוברים לפני שהם מגיעים לדפדפן.
Reflected XSS. השרת לוקח ערך מהבקשה הנוכחית, בדרך כלל פרמטר שאילתה או שדה טופס, ומחזיר אותו בתגובה בלי קידוד בטוח. התוקף בונה קישור שנושא את ה-payload. כדי שהתקיפה תעבוד, הקורבן צריך לפתוח את הקישור. ה-payload לא נשמר בשום מקום בשרת.
Stored XSS. האפליקציה שומרת קלט בשליטת התוקף, כמו תגובה, שם משתמש או כרטיס תמיכה, ומאוחר יותר מציגה אותו למשתמשים אחרים. הקורבן לא צריך קישור מיוחד. מספיק לצפות בדף הרגיל, ולכן ל-stored XSS יש בדרך כלל ההשפעה הגדולה ביותר.
DOM-based XSS. ייתכן שהשרת בכלל לא רואה את ה-payload. JavaScript בצד הלקוח קורא מקור שתוקף יכול לשלוט בו, כמו location.hash, location.search, document.referrer או הודעה שמגיעה דרך postMessage, וכותב אותו ל-sink מסוכן כמו innerHTML, document.write או eval. הבאג נמצא בקוד הדפדפן. מסננים בצד השרת לא רואים את התקיפה, ולכן הסוג הזה מפתיע צוותים שסוקרים רק קוד backend.
לשלושת הסוגים שורש אחד. נתונים לא מהימנים מגיעים להקשר שמפרש אותם כקוד.
קידוד פלט הוא ההגנה העיקרית, והוא לא פעולה אחת. הקידוד הנכון תלוי במקום שאליו הערך מגיע. OWASP XSS Prevention Cheat Sheet מפרט את הכללים לכל הקשר. בקצרה:
| ההקשר שאליו הערך מגיע | מה לעשות | טעות נפוצה |
|---|---|---|
| גוף HTML | קידוד ישויות HTML של &, <, >, ", ' |
קידוד כפול, שמציג &lt; בדף |
| מאפיין HTML | לקודד ותמיד לשים את ערך המאפיין במירכאות | להשאיר את הערך בלי מירכאות, או להניח שזה מגן על href או על event handlers |
| מחרוזת JavaScript | להשתמש במקודד של ספרייה להקשר המדויק הזה | לעשות escaping ל-HTML ואז להכניס את הערך לבלוק <script> |
| פרמטר URL | לעשות percent-encoding לערך עם encodeURIComponent(), ולאפשר רק סכמות http וגם https ב-URLs שלמים |
להתייחס לקידוד כמספיק ל-URL מלא שהגיע ממשתמש: סכמת javascript: עדיין רצה כשהיא מגיעה למאפיין href |
| CSS | לא להכניס נתונים לא מהימנים ל-CSS; לקבוע מאפיינים קבועים דרך ה-CSSOM | לבנות מחרוזת למאפיין style מקלט משתמש |
הטבלה מראה למה "לנקות את הקלט" הוא כלל חלקי. מחרוזת שבטוחה בפסקה יכולה להיות מסוכנת בתוך מאפיין href, כי javascript: היא סכמת URL תקפה שם. אותה מחרוזת לא מזיקה כשהיא נכתבת עם textContent. הקידוד צריך להתאים ל-sink שהערך מגיע אליו ברגע הכתיבה.
ה-Cheat Sheet של OWASP מזהיר גם ש-escapes של hex או Unicode לבדם לא מחליפים מקודד JavaScript שמודע להקשר, ושאין להעביר מחרוזות לא מהימנות בכלל לפונקציות eval(), setTimeout() או setInterval(). מקודדים בפלט ומרחיקים את הפונקציות האלה מנתיבים לא מהימנים.
אפשר לצמצם את מספר ההחלטות בבחירת sinks שמתייחסים לקלט כטקסט. הגדרה של element.textContent או element.value כותבת תווים ולא markup. הגדרה של element.innerHTML מפעילה parser של HTML. ה-Cheat Sheet של OWASP מציין את innerHTML כ-sink שיש להימנע ממנו בנתונים לא מהימנים וממליץ במקומו על textContent או value.
frameworks מודרניים כמו React, Angular, Vue ו-Svelte עושים escaping לערכים ב-bindings של ברירת המחדל בתבניות שלהם. ערך שנכתב בצורה {userName} ב-JSX או כ-interpolation בתבנית הופך לטקסט. הסיכון חוזר כשמפתחים משתמשים במסלול עקיפה: dangerouslySetInnerHTML ב-React, bypassSecurityTrustHtml ב-Angular, v-html ב-Vue, או כתיבה ישירה ל-DOM מספרייה. דרך מסלולי העקיפה האלה XSS נכנסת בדרך כלל לאפליקציית framework. כל שימוש כזה הוא פריט לסקירה, וכדאי להחזיק רשימה שלהם.
Content Security Policy (CSP) היא כותרת תגובה, או אלמנט <meta http-equiv>, שאומרת לדפדפן באילו מקורות של סקריפטים, סגנונות, תמונות ומשאבים אחרים דף רשאי להשתמש. MDN מתאר את מטרתה העיקרית כשליטה במשאבים שמסמך יכול לטעון, וכך היא מגינה מפני XSS. ההנחיה default-src קובעת את ברירת המחדל לכל סוג משאב שאין לו הנחיה משלו.
עם default-src 'self', הדפדפן מסרב לסקריפטים inline ול-event handlers מסוג inline כמו onerror="...", ומסרב ל-URLs של javascript: במקומות שבהם הם היו מריצים קוד. CSP לא מתקנת באג ב-HTML שלך. היא מגבילה את מה ש-payload מוזרק יכול לעשות. לכן זו הגנה בשכבות: קידוד עוצר את ההזרקה, ו-CSP מקטינה את הערך של הזרקה שהתפספסה.
יש שתי מגבלות. מדיניות <meta> לא תומכת בכל היכולות של CSP, ו-MDN מציין שמדיניות report-only חייבת להישלח ככותרות HTTP. לפני הסתמכות על <meta>, בודקים בדף של MDN אילו הנחיות הדפדפנים מכבדים בה. מדיניות שמתירה 'unsafe-inline' או רשימת מארחים רחבה נותנת הרבה פחות הגנה.
למעבדה יש שני חלקים. קודם פותרים את אתגר ה-DOM XSS ב-OWASP Juice Shop, אפליקציה פגיעה בכוונה, שרצה על המחשב שלך. אחר כך בונים דף קטן שכותב את אותו טקסט לא מהימן לשני sinks שונים, ורואים מה הדפדפן עושה.
מריצים כל שלב על מחשב שבשליטתך. השירותים נשארים קשורים לכתובת 127.0.0.1. לא מכוונים את ה-payloads האלה לאתר שאינו בבעלותך או שאין לך הרשאה בכתב לבדוק.
מוודאים ש-Docker מגיב:
docker --version
docker info --format '{{.ServerVersion}}'במחשב שבו השתמשנו למאמר הזה, הפקודה הראשונה הדפיסה Docker version 29.8.2, build 7fc2dff. הגרסה אצלך עשויה להיות שונה.
מפעילים את הקונטיינר עם מיפוי פורט ל-loopback בלבד:
docker run -d -p 127.0.0.1:3000:3000 bkimminich/juice-shopהקידומת 127.0.0.1: מפרסמת את הפורט רק על ממשק ה-loopback שלך. אם פורט 3000 כבר תפוס, בוחרים פורט אחר במארח, כמו 127.0.0.1:3001:3000, ומשנים בהתאם את ה-URLs בהמשך.
בודקים שהאפליקציה עונה וקוראים את הגרסה שלה:
curl -s http://127.0.0.1:3000/rest/admin/application-versionבמחשב שבו השתמשנו למאמר הזה, התגובה הייתה {"version":"20.3.0"}. ההפעלה הראשונה יכולה לקחת קצת זמן, ולכן חוזרים על הבקשה אם אין תשובה.
במדריך הרשמי הנלווה ל-Juice Shop יש סעיף בשם "Perform a DOM XSS attack." הרמז שלו אומר לחפש שדה קלט שהתוכן שלו מופיע ב-HTML כשהטופס נשלח. המדריך נותן את ה-payload הזה:
<iframe src="javascript:alert(`xss`)">עובדים לפי המדריך בדפדפן שלך. פותחים את שורת החיפוש של Juice Shop, מדביקים את ה-payload ושולחים. גם URL של חיפוש עם השאילתה ב-fragment עובד. במחשב שבו השתמשנו למאמר הזה, ה-URL הזה נטען ב-Chrome ב-headless, ולוח התוצאות דיווח אחר כך שהאתגר נפתר:
http://127.0.0.1:3000/#/search?q=%3Ciframe%20src%3D%22javascript%3Aalert(%60xss%60)%22%3Eשואלים את ה-API של האפליקציה על האתגר כדי לבדוק את הסטטוס שלו:
curl -s "http://127.0.0.1:3000/api/Challenges/?name=DOM%20XSS" | grep -o '"solved":[a-z]*'לפני טעינת URL החיפוש הפלט היה "solved":false. אחרי הטעינה הפלט היה "solved":true. התוצאה אצלך אמורה להתאים אחרי שהאתגר הופעל בדפדפן.
כדאי לשים לב מה הפך את זה לבאג DOM-based. ה-payload נמצא ב-fragment, שהדפדפן לא שולח לשרת, והקוד בצד הלקוח כותב אותו לדף. לוג שרת היה מראה רק בקשות ל-HTML ולסקריפטים של האפליקציה.
יוצרים תיקייה ונכנסים אליה:
mkdir xsslab && cd xsslabיוצרים את index.html. הקובץ הזה קורא את הטקסט שאחרי #, כותב אותו פעם אחת עם innerHTML ופעם אחת עם textContent, ומוסיף את אלמנט ה-CSP meta מתדריך המעבדה:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<title>XSS sink lab</title>
</head>
<body>
<h1>Sink comparison</h1>
<p>Put text after the # in the URL, for example <code>#hello</code>.</p>
<h2>innerHTML</h2>
<div id="html-sink"></div>
<h2>textContent</h2>
<div id="text-sink"></div>
<script src="app.js"></script>
</body>
</html>יוצרים את app.js באותה תיקייה. הסקריפט נמצא בקובץ נפרד בכוונה. המדיניות הייתה חוסמת גם בלוק <script> מסוג inline, וזה היה מסתיר את ההשוואה שרוצים לעשות.
// Read the fragment once, then write it to two different sinks.
const raw = decodeURIComponent(location.hash.slice(1));
document.getElementById('html-sink').innerHTML = raw;
document.getElementById('text-sink').textContent = raw;מגישים את התיקייה על loopback בלבד. ב-Windows ה-launcher של Python הוא בדרך כלל python; ב-Linux וב-macOS הוא לעיתים קרובות python3:
python -m http.server 8000 --bind 127.0.0.1משאירים את הטרמינל הזה פתוח בזמן העבודה. פותחים את http://127.0.0.1:8000/index.html בדפדפן. בסיום עוצרים את השרת עם Ctrl+C.
פותחים את כלי המפתחים של הדפדפן ואת לשונית Console. קודם טוענים את הדף עם ערך לא מזיק:
http://127.0.0.1:8000/index.html#helloאחר כך מריצים בקונסולה את שתי השורות האלה:
document.getElementById('html-sink').innerHTML
document.getElementById('text-sink').textContentשתיהן מציגות hello. עכשיו טוענים fragment שמערבב markup ו-event handler:
http://127.0.0.1:8000/index.html#hello%20%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3Eטענתי את ה-URL המדויק הזה ב-Chrome 156.0.8078.80 במצב headless ושמרתי את ה-DOM שהתקבל. הפלט הרלוונטי היה:
<div id="html-sink">hello <img src="x" onerror="alert(1)"></div>
<div id="text-sink">hello <img src=x onerror=alert(1)></div>ה-sink של innerHTML יצר אלמנט img. ה-sink של textContent שמר את אותם תווים כטקסט פשוט. מריצים שוב את שתי שורות הקונסולה על ה-URL הזה ובודקים שהראשונה מחזירה markup עם אלמנט בתוכו, והשנייה מחזירה את המחרוזת המקודדת.
הדף שלמעלה כבר כולל default-src 'self'. אותה הרצת headless רשמה את ההודעה הזאת:
Executing inline event handler violates the following Content Security Policy directive 'default-src 'self''.האלמנט img נוצר, אבל ה-handler של onerror לא הריץ את alert(1). המדיניות עצרה את ה-handler מסוג inline. בקונסולה שלך אמורה להופיע הודעה דומה. הניסוח המדויק יכול להשתנות בין גרסאות דפדפן.
כדי לראות את ההבדל, מסירים את אלמנט ה-meta, טוענים שוב את אותו URL ומחכים ל-alert. אחר כך מחזירים את אלמנט ה-meta. ההשוואה הזאת היא השלב היחיד במעבדה שבו ה-payload אמור לרוץ.
נקודה אחת שכדאי לזכור. המדיניות לא הפכה את ה-sink של innerHTML לבטוח. היא חסמה סוג אחד של הרצה. האלמנט img עדיין קיים ב-DOM, ותוקף שמוצא payload שהמדיניות מתירה עדיין יכול לפעול דרכו. התיקון הנכון הוא לכתוב טקסט לא מהימן עם textContent.
escaping אחד להקשר הלא נכון. קידוד HTML מגן על טקסט בפסקה. הוא לא מגן על URL, על מחרוזת JavaScript או על ערך CSS. מתאימים את המקודד למקום שאליו הערך מגיע.
סינון מחרוזות רעות מוכרות. רשימות חסימה שמסירות <script> מפספסות את <img onerror>, את <svg onload> ו-URLs של javascript:. קידוד ו-sinks בטוחים עובדים כי הם לא תלויים בניחוש המחרוזת הבאה של התוקף.
אמון עיוור ב-framework. ה-bindings של ברירת המחדל עושים escaping לערכים, אבל dangerouslySetInnerHTML, v-html וגם bypassSecurityTrustHtml מדלגים על ההגנה הזאת. מחפשים אותם בקוד שלך.
קריאת קוד שרת בלבד. DOM XSS אף פעם לא מגיעה לשרת. סוקרים קוד לקוח שקורא נתונים מהמקורות location, document.referrer וגם postMessage.
CSP כבקרה היחידה. מדיניות קפדנית עוזרת, אבל היא לא מחליפה קידוד, ומדיניות חלשה עם 'unsafe-inline' נותנת הגנה מועטה.
הנחה שלמדיניות meta יש את כל היכולות. מדיניות באלמנט <meta> לא יכולה להשתמש במצב report-only או בכל ההנחיות. כשצריך דוחות, שולחים את המדיניות ככותרת תגובה.
קוראים את OWASP Cross Site Scripting Prevention Cheat Sheet מההתחלה עד הסוף. הוא קצר ביחס למסמך אבטחה, והוא מכסה כל הקשר בטבלה שלמעלה. אחר כך קוראים את מדריך ה-CSP של MDN ומנסים מדיניות קפדנית יותר עם nonces או hashes בדף המעבדה שלך.
עוברים על שאר אתגרי Juice Shop שקשורים ל-XSS, ומשווים איך הגרסאות stored ו-reflected שונות ממקרה ה-DOM שנפתר כאן. המעבדה נשארת על loopback, ובסיום עוצרים את הקונטיינר:
docker ps --filter ancestor=bkimminich/juice-shop
docker stop <container-id>המאמר הבא בסדרה עוסק ב-cross-site request forgery ובאופן שבו עוגיות SameSite מצמצמות אחד הנתיבים שלה. לעיתים קרובות מבלבלים בין XSS ל-CSRF, וכדאי להכיר את ההבדל לפני שמתכננים מודל סשנים.
אבטחת Web ו-API
ה-JavaScript בצד הלקוח מפרט את ה-API שהאפליקציה מדברת איתו. בעזרת מודל AI קוראים bundles ומעלים endpoints ופרמטרים, ואז מאמתים כל אחד בעצמך.
אבטחת Web ו-API
מתודולוגיה ל-IDOR, XSS, SSRF ו-auth שבור עם מודל AI כקורא וכדעה שנייה, בתוספת איך מאמתים finding ונמנעים מ-false positives.
אבטחת Web ו-API
APIs חושפים את הצורה שלהם דרך OpenAPI specs ו-GraphQL introspection. בעזרת מודל AI קוראים את ה-schema ומתכננים בדיקות authorization שאתה מאמת.