בפוסט זה נסביר מה היא פרצת XSS – Cross Site Scripting ונראה הדגמה לחולשה הזו באתר פורומים אמיתי. החולשה מאפשרת לכל משתמש בפורום להזריק קוד JavaScript בדפדפן של משתמש אחר ולהריץ אותו, מה שמאפשר אפילו להתחבר בחשבון של המשתמש האחר. נראה איך החולשה הזאת קיימת כתוצאה מחוסר שימוש בספריות מעודכנות לאבטחת הקוד ואיך אפשר בקלות יחסית להתגבר עליה.
מהי התקפת XSS
כאשר אנו צופים בדף של אתר, הדפדפן שלנו מריץ קוד JavaScript שהאתר בו אנחנו מבקרים שלח לדפדפן. קוד ה-JavaScript שרץ בדפדפן הוא הקוד שיוצרי האתר סמכו עליו והחליטו שהוא יכול לרוץ. הקוד יכול להיות סקריפט שהם עצמם כתבו, או סקריפט שצד שלישי כתב אך כותבי האתר סומכים עליו שלא יעשו דברים לא רצויים. למשל, סקריפט של כלי הפרסום של גוגל וכדומה.
הבעיה מתעוררת כאשר גורם שלישי, תוקף בלתי מורשה, מצליח להכניס לאתר קוד שלו, שכותבי האתר לא מודעים אליו, אבל מבחינת הדפדפן שלנו הסקריפט הזה נחשב כחלק לגיטימי מהאתר ולכן הוא מריץ אותו.
למה זה בעייתי?
קוד כזה יכול לשלוח לשרת ששייך לתוקפים את מזהה ההתחברות שלנו מול האתר (למשל cookie). במצב כזה התוקפים יכולים להתחבר בשם שלנו לאתר, לפרסם תוכן, לקרוא הודעות, וכדומה. התוקפים יכולים לגלות את הסיסמה שאיתה אנחנו מתחברים לאתר. במקרה שעשינו את הטעות של התחברות למערכות שונות באותה סיסמה התוקף יכול להתחבר עם אותה סיסמה לחשבונות שלנו במערכות נוספות. צורת תקיפה אפשרית נוספת היא כאשר הסקריפט שולח את כל הקשות המקלדת שלנו לשרת שיזהה מההקשות נתונים רגישים כמו מספר כרטיס האשראי שלנו.
פרצת XSS יכולה להיות בכל שפה, למשל פרצת log4shell הידועה. אנחנו נתמקד בפרצות שקורות בקוד JavaScript באתרי אינטרנט.
איך מתרחשת התקפת XSS בדפדפן?
לרוב פרצת XSS תקרה על ידי הכנסת תוכן גולשים לאתר. נחשוב על אתר שמציג פרטים של משתמש אחר. נניח שהוא עושה את זה בדרך הבאה: document.querySelector(“#username”).innerHTML = user.username
. משתמש שיבין כי האתר מציג בצורה כזאת את השם שלו, יוכל להכניס את שם המשתמש המתוחכם והזדוני David <script>console.log(document.cookie) </script> Cohen
אם האתר לא יהיה מודע לכך ויקבל את שם המשתמש כמות שהוא, הרי שכאשר הוא ינסה להציג אותו כאשר ניכנס לעמוד, הקוד שהוחדר יתבצע והעוגייה שלנו תודפס לקונסול. הדוגמא הזאת היא בלתי מזיקה, אבל קוד אחר יכול לשלוח את ה-cookie לשרת חיצוני או לעשות דברים זדוניים אחרים, כפי שתיארנו לעיל.
ההתקפה הפשוטה והנאיבית הזו ניתנת לפתרון בקלות: במקום להכניס את שם המשתמש באמצעות innerHTML
יש להשתמש ב-innerText
בצורה הזאת: document.querySelector(“#username”).innerText = user.username
. בדרך זו הדפדפן מתייחס תמיד לשם המשתמש כטקסט ולא כקוד. וכך, שם המשתמש שיוצג הוא "David <script>…</script> Cohen" והדפדפן לא יתייחס אליו כקטע קוד ולא יריץ אותו. גם כאשר הדף נוצר בצד השרת יש דרכים דומות למנוע מהדפדפן לפרש את הטקסט הזה כקטע קוד, אך לצורך הבנת העיקרון די לנו בדוגמא של צד הקליינט.
תוכן גולשים עשיר
פעמים רבות בעלי האתר ירצו לתת את האפשרות למבקרים באתר להכניס תוכן מעוצב או להעתיק אלמנטים מדפים אחרים. הדרך הקלה ביותר לעשות זאת היא לאפשר למבקרים באתר להזין קוד HTML שהאתר יכניס כחלק מהדף. רבים מעורכי הטקסט שאנחנו רואים במערכות פורומים עובדים בצורה זו.
כדי שהפיצ׳ר הזה יעבוד כראוי, הדף חייב לפרש את הטקסט המוזן בתור אלמנטים של HTML ולא כטקסט פשוט, ולכן הקוד: <div>This is <b>camelCase</b></div>
יגרום שהמילים "camelCase" יודגשו.
אולם, כעת המשתמשים יכולים גם להכניס סקריפטים לתוך ההודעה, למשל:
<div>This is camelCase<script>....</script></div>
במקרה הזה הסקריפט שיש באמצע ההודעה ירוץ אצל כל המשתמשים שיקראו את ההודעה. למרות שיוצרי האתר לא יודעים שהוא קיים, הדפדפן חושב שזה סקריפט שהגיע מהאתר עצמו ולכן לגיטימי להריץ אותו. בתור בעלי האתר אנו חייבים למצוא דרך למנוע מכותבי ההודעה להריץ קוד JavaScript באתר, למרות שאנו מאפשרים להם להכניס HTML ״מתוחכם״ ולא רק טקסט פשוט.
מה עושים
בתור כותבי האתר, אנחנו חייבים לנקות כל טקסט שמוכנס על ידי המשתמשים. בטרמנילוגיה המקובלת קוראים לזה סניטיזציה – sanitization ובעברית חיטוי. כלומר, על האתר לוודא שלפני שהוא מציג תוכן שנכתב על ידי גולשים אחרים או כל גורם חיצוני אחר הוא מוריד ממנו כל סקריפט שהוא ומשאיר רק HTML שלא יכול להריץ קוד JavaScript. כפי שנראה, זוהי לא משימה טריוויאלית משום שקוד יכול להגיע בצורות שונות, וכדי שהסניטיזציה שלנו תחסום כל אפשרות להחדרת קוד היא צריכה להכיר ולמנוע את כולם.
איך לא לעשות סניטיזציה
הדרך הכי פשוטה לפתור את הבעיה היא לסנן תגיות מסוג script: כאשר נראה את המחרוזת <script> באמצע הטקסט פשוט נמחק אותו וכך נבטיח שהטקסט שיוכנס לעמוד שלנו לא יכיל אלמנטים של קוד. הבעיה היא שאפשר להגדיר קוד JavaScript גם ללא שימוש בתגיות script. למשל, אפשר להגדיר לכל אלמנט מאפיין onclick ולגרום לקוד שבתוכו לרוץ במקרה שהמשתמש יקיש עליו. הנה דוגמה לקוד כזה.
<div onclick="console.log(document.cookie)"></div>
גישה בעייתית נוספת היא לאפשר להריץ סקריפט אבל לנסות לשלוט מה הסקריפט מורשה לעשות ומה לא. למשל, במערכת הפורומים של בחדרי חרדים נהגו להחליף כל מקום שכותבים בהודעה document.cookie
בכוכביות. הבעיה בגישה זו היא שמאוד קל ליצור קוד שעושה מה שאנחנו רוצים בלי לכתוב את המילים "האסורות". למשל במקום לכתוב document.cookie
נכתוב document["coo" + "kie"]
, כך שמנוע החוקים של האתר כנראה לא יצליח להבין שמדובר בקוד בעייתי.
דוגמה למתקפת xss – בחדרי חרדים
קצת היסטוריה: מערכת הפורומים "בחדרי חרדים" קיימת משנת 2002 בגלגולים שונים. היא הייתה בעבר הזירה הכי חמה במגזר החרדי לחדשות, עדכונים, פוליטיקות פנים חרדיות. עם השנים הועם זוהרה של מערכת הפורומים של ״בחדרי חרדים״, אך היא עדיין פעילה ונכתבות בה מאות הודעות ביום שחלקן מגיעות לעשרות אלפי צפיות.
כמו במערכות פורומים אחרות, המשתמשים יכולים לכתוב הודעות בתור קוד HTML. כותבי האתר הביאו מראש בחשבון אפשרות של פרצת xss והם הפעילו סניטיזציה כלשהי. למשל, הנה קוד HTML שניסינו להכניס:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<img | |
id="xss-image-2" | |
src="/" | |
onerror="console.log(document.cookie); | |
document.querySelector('#xss-image-2').src = 'https://upload.wikimedia.org.wikipedia/commons/c/ca/1×1.png" | |
/> | |
<script>console.log("message")</script> |
אחרי הסניטיזציה של האתר הקוד נראה כך:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<img | |
id="xss-image-2" | |
src="/" | |
onerror="console.log(***************); | |
document.queryor('#xss-image-2').src = '<a href=" https:="" upload.wikimedia.org.wikipedia="" commons="" c="" ca="" 1×1.png';"="" target="_blank" | |
> |
נשים לב שהתג script
נעלם לגמרי, ה-document.query
הוחלף בכוכביות, והמילה querySelector
הוחלפה ב-queryor
. בנוסף לשינויים הנובעים מסניטיזציה, הלינק לויקיפדיה הוחלף בתג a
עם href
, כנראה מתוך רצון להציג קישורים בצורה אחידה.
כאמור, קל למדי לעקוף מנוע חוקים כזה. כדי לעקוף את הסניטיזציה הזאת, כתבנו את הקוד הבא. שני קטעי הקוד שקולים לוגית, והסניטיזציה של בחדרי חרדים לא זיהתה אותו כקוד בעייתי.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<img | |
id="xss-image" | |
src="/" | |
onerror="d = document; | |
c = ('cooki' + 'e').trim(); | |
qs = 'queryS' + 'elector'; | |
console.log(d[c]); | |
d[qs + 'All']('.top_nlsitem').forEach(n => n.style.backgroundColor = 'green'); | |
d[qs]('#xss-image').src = 'ht' + 'tps://upload.wikimedia.org/wikipedia/commons/c/ca/1×1.png';" | |
/> |
בקוד הזה שמנו אלמנט img
עם src
לא תקין, מה שגורם לקריאה לפונקציה onerror
שהגדרנו. בפונקציה הזאת אנחנו מדפיסים את ה-cookie ל-console. כאמור, פעולה זו אינה בעייתית כשלעצמה אך מהווה דוגמא למה שגורם זדוני היה יכול לעשות – לשלוח לעצמו את הערך של ה-cookie שאמור להישאר חסוי. על מנת להתגבר על הסניטיזציה שלהם אנחנו נאלצים לא לכתוב במפורש את המילה cookie ועושים מניפולציה קטנה על מחרוזות כדי ליצור אותה. לצורך ההדגמה הוספנו גם קוד שמוסיף רקע ירוק לכל שמות המשתמשים בדף. גם כאן אנחנו עושים מניפולציה קטנה בשביל לעקוף את מנוע החוקים של האתר ולהשתמש ב-querySelector
למרות שהסניטיזציה מנסה לחסום את השימוש בו. אחר כך אנחנו מגדירים src
תקין לתמונה כדי שהמשתמשים לא יבחינו בתמונה לא תקינה.
אל תרוצו לבדוק את זה. דיווחנו בצורה מסודרת לבחדרי חרדים על הפרצה, והם מיהרו לסגור את האפשרות ליצור הודעות חדשות כאלו. חשוב לציין שהם לא איימו עלינו או האשימו אותנו, ואפילו הודו לנו על הדיווח. בדיקה כזו צריכה להתבצע באחריות בלי לגרום נזק לכם, לאתר ולאחרים וכמובן בלי לעבור על החוק. בשרשור הזה כתבנו הודעה שמכילה קוד שצובע את שם המשתמש בצבע ירוק ומדפיס את העוגייה ל-console. אחרי הדיווח ההודעה נמחקה, אך ניתן לראות בצילום המסך שעשינו את שם המשתמש בירוק ואת ההדפסות.

כפי שאמרנו, האקר זדוני לא היה מסתפק בשינויים הקוסמטיים שביצענו אלא היה כנראה שולח את ה-cookie לשרת ששייך לו וכך היה יכול להזדהות בתור משתמשים אחרים, לקרוא את ההודעות הפרטיות שלהם, לפרסם תכנים בשמם, לדעת את כתובת האימייל שלהם וכולי. כך שאם יש לך חשבון במערכת הפורומים של בחדרי חרדים ואם אכן האקר כלשהו ניצל לרעה את החולשה שטופלה, ייתכן ומישהו יודע מה כתובת המייל שלך וקרא את ההודעות הפרטיות שלך.
איך *כן* מתגברים על XSS
סניטיזציה חייבת להיעשות על ידי ספרייה מוכרת ועדכנית. עם הזמן מתווספות עוד תכונות לדפדפנים ומתגלות עוד חולשות שעלולות לאפשר לגורמים זדוניים להפעיל XSS. כמו כן ספריות רבות מאפשרות להגדיר את רמת ההגנה שנדרוש. למשל, בספריית ה-JS הזאת יש אפשרות למנוע לחלוטין העלאת תמונות. אם נבחר לאפשר העלאת תמונות, היא עדיין תגן עלינו באמצעות זאת שהיא תמנע הגדרת onerror על התמונה שהועלתה. ספריות דומות קיימות בפייתון ובשפות רבות.
הנה דוגמה לסנטיזציה לקוד שלנו תוך שימוש בספריה איכותית ועדכנית:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const sanitizeHtml = require("sanitize-html") | |
const evilHtml = ` | |
<img | |
id="xss-image" | |
src="/" | |
onerror="d = document; | |
c = ('cooki' + 'e').trim(); | |
qs = 'queryS' + 'elector'; | |
console.log(d[c]); | |
d[qs + 'All']('.top_nlsitem').forEach(n => n.style.backgroundColor = 'green'); | |
d[qs]('#xss-image').src = 'ht' + 'tps://upload.wikimedia.org/wikipedia/commons/c/ca/1×1.png';" | |
/> | |
` | |
const legitHtml = sanitizeHtml(evilHtml, { allowedTags: "img" }) | |
console.log(legitHtml) | |
/* Output: | |
"\n<img src=\"/\" />\n" | |
*/ |
חשוב להדגיש כי כדאי לנקות את התוכן גם לפני ששומרים אותו ב-DB וגם לפני שמציגים אותו למשתמש.
אנו מקווים שלמדתם מה זה XSS, כיצד להפעיל סניטיזציה על תוכן גולשים ובעיקר מה לא לעשות. מכיוון שאנחנו איננו אנשי אבטחת מידע שקיבלו אישור מהאחראי על האינטרנט, חלק מהדרך בה תעשו סניטיזציה ברמה גבוהה היא להיוועץ באלו שהם כן אנשי אבטחת מידע ״אמיתיים״.
תודה לרן בר זיק על העזרה בכתיבת המאמר.
יופי של פוסט! מבהיר היטב את הבעיה, מסביר את החלקים הטכניים בצורה פשוטה וברורה בלי לוותר על הדיוק, וכתוב נהדר. והסיום – פרייסלס
מאמר ברור ובהיר.
אגב -אני חושב שלא העליתם את הסנטיציה שעשה בחדרי חרדים לקוד שלכם
מרתק
מחכים וכתוב נהדר
מרתק. אני צריך לדאוג ? 😉
מאמר נהדר, מקיף ו"בגובה העיניים" למרות שאינכם אנשי אבטחת מידע "אמיתיים" 😉
כל הכבוד, תודה רבה.
השאלה אם בימינו הרשת הולכת ומצטמצמת לשימוש בכמה כלים ספציפיים ב-open source, למשל phpBB לפורומים ו-WordPress לאתרים רגילים, ושם יש מתכנתים שחושבים מראש על הדברים האלה, לעומת כלים In house.
מאמר מעולה.
בנוסף לסניטציה שהראיתם כאן,
מומלץ גם להגדיר CSP ברמת השרת שימנע מסקריפטים INLINE לרוץ בכלל [אם איכשהו הצליחו להכניס לכם בכל זאת].
ניתן להגדיר קונפיגורציות שונות – לפי מה שמתאים לצרכים שלכם – להרחבה בנושא:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
[אמנם יש עוד הרבה המלצות בתחום – אבל אני חושב שכדאי להוסיף עוד המצלה בסיסית וחשובה ביותר]
בנוסף למה שאמרתי קודם, מומלץ ממש להגדיר את הקוקיז כ- HttpOnly, כך שלא יהיה ניתן לגשת אליהם דרך JS בכל מקרה.
לעולם, אבל לעולם אסור לתת למשתמש להכניס JS, ובאמת צריך להשתמש לצורך זה בספריות השונות שנכתבו בדיוק לשם כך.
ההחלפות טקסט האלה הן לא יותר מבדיחה – כי אפשר לדוגמה לכתוב **כל** קוד שרוצים עם JSFuck (https://he.wikipedia.org/wiki/JSFuck) והמנגון לא יעלה על זה לעולם…
השכלתי. תודה!
לא הצלחתי להבין את פוטנציאל ההתקפה. לפי מה שכן הבנתי מהדוגמא במאמר, ההאקר תקף את עצמו ושלח את העוגייה שלו לשרת אחר. האם יש כאן משהו מעבר לזה?
ההאקר תקף את עצמו אבל באותה מידה הוא יכל לתקוף כל גולש אחר שנכנס למערכת הפורומים ואחר כך להתחזות בשמו, לקרוא הודעות ]רטיות, לכתוב הודעות, לראות את כתובת האימייל.