DOM-based XSS Demonstration
"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.
none
(no check performed)
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.
// 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');
}
});
event.source
matches the expected window reference (e.g., event.source === iframe.contentWindow).textContent is always safer than innerHTML.script-src 'self' as a minimum.X-Frame-Options: DENY to prevent clickjacking + postMessage attacks.sandbox attribute without allow-same-origin.These techniques have been found in production apps: