לדלג לתוכן

ניצול PostMessage מוסבר - איך תקשורת בין חלונות הופכת לחור אבטחה

כמעט כל אפליקציית ווב מודרנית משתמשת ב-iframe בשלב כלשהו - חלונות תשלום, וידג'טים של צד שלישי, מערכות SSO, או פשוט הפרדה בין חלקים שונים של אותו אתר. הבעיה היא שדפדפנים אוסרים על חלונות ו-iframes ממקורות שונים לגשת ישירות זה לזה, ולכן נולד ה-API שנקרא postMessage. הוא נועד לפתור בעיה אמיתית, אבל כשמממשים אותו ברשלנות, הוא הופך לאחד מהחורים הכי נפוצים שבודקי חדירה מוצאים באפליקציות עשירות בג'אווהסקריפט.

מה זה בעצם postMessage

postMessage הוא API שמאפשר לחלון אחד לשלוח הודעה לחלון אחר, גם אם הם ממקורות שונים לגמרי. חלון האב יכול לשלוח הודעה ל-iframe שבתוכו, iframe יכול לשלוח הודעה חזרה להורה, ואפילו שני טאבים נפרדים שנפתחו זה מזה יכולים לתקשר כך. זה מנגנון לגיטימי ושימושי, אבל כמו כל ערוץ תקשורת שחוצה גבולות מקור, הוא דורש בדיקות זהירות משני הצדדים - גם מי ששולח וגם מי שמקבל.

הבעיה הראשונה - לא בודקים מאיפה ההודעה הגיעה

הצד שמקבל הודעות דרך window.addEventListener("message", ...) מקבל אובייקט אירוע שמכיל שדה בשם origin, שמציין בדיוק מאיזה מקור ההודעה נשלחה. הבעיה היא שהרבה מפתחים מתעלמים מהשדה הזה לגמרי, ומטפלים בכל הודעה שמגיעה כאילו היא באה מהמקור הצפוי.

זו טעות קריטית, כי כל דף באינטרנט יכול לפתוח חלון או iframe לאתר שלכם ולשלוח אליו הודעות. אם הקוד שלכם מקבל הודעה ומבצע פעולה על סמך התוכן שלה בלי לבדוק מי שלח אותה - למשל מעדכן מצב התחברות, מציג תוכן, או מריץ קוד על סמך שדה בתוך ההודעה - תוקף יכול לפתוח חלון קטן לאתר שלכם ולשלוח הודעות מזויפות שגורמות לאפליקציה להתנהג בדיוק כמו שהוא רוצה.

// קוד פגיע - מקבל כל הודעה בלי לבדוק origin
window.addEventListener("message", (event) => {
  document.getElementById("output").innerHTML = event.data.html;
});

בדוגמה הזו, כל אתר בעולם יכול לפתוח iframe או חלון לדף הזה ולשלוח event.data.html עם תוכן זדוני, וזה ייכתב ישירות ל-DOM.

הבעיה השנייה - שולחים מידע רגיש לכל אחד

הצד השני של המטבע הוא בעת שליחת הודעה. הפונקציה postMessage מקבלת פרמטר שני שנקרא target origin, שאמור לציין באופן מפורש למי מותר לקבל את ההודעה. הרבה מפתחים משתמשים בכוכבית * כדי "לחסוך זמן", מה שאומר שההודעה תישלח לכל מי שיושב באותו חלון, בלי קשר למקור שלו.

אם ההודעה מכילה מידע רגיש - טוקן אימות, פרטי משתמש, תוכן פרטי - ותוקף הצליח לגרום לאפליקציה לפתוח את החלון או ה-iframe שלו במקום היעד המקורי, למשל דרך חולשת ניתוב פתוח, ההודעה עם המידע הרגיש תישלח ישירות אליו.

איך תוקף בפועל מנצל את זה

תרחיש נפוץ נראה כך - אתר A משבץ iframe של אתר B לצורך תהליך תשלום או אימות. כשהתהליך מסתיים, אתר B שולח לאתר A הודעת postMessage עם תוצאה כמו { status: "approved", token: "..." }. אם אתר A לא בודק את ה-origin של ההודעה, תוקף יכול לבנות דף שמארח iframe או חלון מוסתר לאתר A, ולשלוח אליו הודעת postMessage מזויפת שמדמה תשובה חיובית מאתר B - ובכך לעקוף את כל תהליך האימות בלי לגעת בשרת בכלל.

איך מגנים נכון

ההגנה כאן פשוטה יחסית, אבל צריך ליישם אותה משני הכיוונים בו זמנית:

  • בצד המקבל - תמיד לבדוק את event.origin מול רשימת מקורות מורשים מפורשת, ולהתעלם מכל הודעה שלא תואמת אותה. אף פעם לא לסמוך על תוכן ההודעה עצמה כדי לזהות את השולח.
  • בצד השולח - תמיד לציין target origin מפורש ומדויק, ולעולם לא *, במיוחד כשההודעה מכילה כל מידע שאסור שייחשף לצד שלישי.
  • אימות מבנה הנתונים - גם אחרי בדיקת origin, כדאי לוודא שההודעה מכילה בדיוק את השדות הצפויים, ולא לסמוך על מבנה חופשי שיכול להשתנות.

בדיקה מדוקדקת של תקשורת postMessage היא בדיוק סוג הפרט הקטן שמבדיל בין מבחן חדירה שטחי לבין כזה שבאמת מוצא את הבעיות המהותיות באפליקציה. זה נושא שאנחנו נכנסים אליו לעומק בקורס פריצת אתרים מתקדמת, כי הרבה מהחולשות המעניינות ביותר באפליקציות עשירות בג'אווהסקריפט מסתתרות בדיוק בערוצי תקשורת כאלה שנראים תמימים.

יש לכם אפליקציה עם iframes ותהליכי תקשורת מורכבים ואתם לא בטוחים אם היא חשופה? זו בדיוק השאלה שכדאי לשאול בדיסקורד שלנו.

הצטרפו לקהילה בדיסקורד

לסיכום

postMessage הוא כלי חיוני לתקשורת בין חלונות במקורות שונים, אבל הוא לא מגן על עצמו. כל צד בשיחה חייב לוודא במפורש מי נמצא בקצה השני - המקבל דרך בדיקת origin, והשולח דרך ציון target origin מדויק. הזנחה של אחד מהצדדים האלה הופכת ערוץ תקשורת לגיטימי לדלת פתוחה.