XSS Security Lab

DOM-based XSS Demonstration

Test Your Knowledge

VULNERABLE - DOM XSS via postMessage

Lab #9: Cross-Origin Messaging Gone Wrong

"Messages from anywhere are welcome here" — even malicious ones

Modern web applications often use window.postMessage to enable communication between different windows, iframes, or tabs. It's a powerful API that allows cross-origin messaging, but it comes with a critical responsibility: always verify the origin of incoming messages.

This lab features a live chat widget that listens for messages via postMessage and renders them directly into the page using innerHTML — without checking where the message came from. Any website you visit can send a malicious message to this widget, leading to full DOM XSS. This is exactly how several real-world vulnerabilities have been discovered.

Attack Scenario: Imagine you're logged into a banking app that uses a vulnerable chat widget. You open a new tab and visit an attacker's site. That site silently sends a postMessage to the banking tab, triggering XSS and stealing your session token — all without you knowing.

Live Support Chat Online
Chat messages will appear here...
System: Widget listening for messages...
Attacker Site Simulator Malicious Page

This simulates a malicious website sending a postMessage to the vulnerable chat widget. In reality, this could be any site you visit in another tab.

Real Attacker HTML Code:
<!-- Attacker's website (attacker.com) -->
<iframe id="victim" src="http://victim.com/lab9.html"></iframe>
<script>
  // Wait for iframe to load, then attack
  setTimeout(() => {
    document.getElementById('victim')
      .contentWindow.postMessage(
        '{"type":"message","user":"System","text":"<img src=x onerror=alert(1)>"}',
        '*'  // Send to any origin
      );
  }, 2000);
</script>
Last Message Origin Check: NO ORIGIN VERIFICATION!
Event origin: none
Trusted origins: (no check performed)
Raw postMessage Data:
No message received yet...
// VULNERABLE CODE - No origin check!
window.addEventListener('message', function(event) {
    // VULNERABILITY: Accepting messages from ANY origin
    // Missing: if (event.origin !== "https://trusted.com") return;
    
    try {
        var data = JSON.parse(event.data);
        
        // Renders user-controlled data directly into innerHTML
        document.getElementById('chatMessages').innerHTML += 
            '<div class="message">' +
            '<strong>' + data.user + ':</strong> ' +
            data.text +   // <-- XSS! No sanitization
            '</div>';
            
    } catch(e) {
        // Even non-JSON messages are rendered!
        document.getElementById('chatMessages').innerHTML += 
            '<div style="color:#888;">' + event.data + '</div>';
    }
});

// Real-world scenario: This widget could be embedded in a banking app,
// admin panel, or any application that uses cross-window messaging.

The Fix - Proper postMessage Security

1. Always Validate event.origin

// FIXED CODE - Origin validation
window.addEventListener('message', function(event) {
    // STEP 1: Whitelist trusted origins
    const trustedOrigins = [
        'https://myapp.com',
        'https://trusted-partner.com'
    ];
    
    if (!trustedOrigins.includes(event.origin)) {
        console.warn('Rejected message from untrusted origin:', event.origin);
        return; // Block messages from unknown sources
    }
    
    // STEP 2: Validate the data structure
    try {
        var data = JSON.parse(event.data);
        
        // Ensure expected properties exist
        if (!data.type || !data.user || !data.text) {
            return; // Invalid message format
        }
        
        // STEP 3: Use textContent instead of innerHTML
        const messageDiv = document.createElement('div');
        messageDiv.className = 'message';
        
        const userSpan = document.createElement('strong');
        userSpan.textContent = data.user + ': '; // Safe: rendered as text
        
        const textNode = document.createTextNode(data.text); // Safe: always text
        
        messageDiv.appendChild(userSpan);
        messageDiv.appendChild(textNode);
        
        document.getElementById('chatMessages').appendChild(messageDiv);
        
    } catch(e) {
        // Even error cases should be handled safely
        console.error('Invalid message format');
    }
});

2. Additional Security Measures

  • Use event.source verification: Check that event.source matches the expected window reference (e.g., event.source === iframe.contentWindow).
  • Implement message validation: Verify the structure and types of incoming data. Use a schema validator or at minimum check for expected properties.
  • Always use textContent: When inserting user-controlled data into the DOM, textContent is always safer than innerHTML.
  • Apply Content Security Policy: A strong CSP can prevent XSS even if postMessage validation fails. Include script-src 'self' as a minimum.
  • Use X-Frame-Options: If your app shouldn't be framed, set X-Frame-Options: DENY to prevent clickjacking + postMessage attacks.
  • Sandbox untrusted iframes: If you must embed untrusted content, use the sandbox attribute without allow-same-origin.

3. Testing the Fix

Fix is currently DISABLED - Vulnerable mode active

Real-World Examples

These techniques have been found in production apps:

  • 2018 - YouTube: postMessage XSS allowed attackers to modify video descriptions
  • 2020 - Shopify: Missing origin check in partner dashboard chat widget
  • 2021 - Multiple Crypto Exchanges: TradingView widget postMessage vulnerabilities
  • 2022 - Browser Extensions: postMessage listeners without origin checks in Chrome extensions