October 23, 2026
$8,400 for a Message With No Return Address Check: postMessage Origin Validation Bypass to Account…
Browsers give web pages a way to talk to each other across different origins when they genuinely need to — an embedded payment widget…

By T4nv1
4 min read
Browsers give web pages a way to talk to each other across different origins when they genuinely need to — an embedded payment widget communicating with its parent page, a popup OAuth flow passing a result back to the window that opened it. The postMessage API is how that conversation happens, deliberately allowed to cross the same-origin policy's usual boundaries. The entire safety of that conversation rests on one specific habit: the page receiving a message is supposed to check who sent it before trusting anything inside. This bug existed because that check was there, visibly, in the code — just written wrong.
The target, which I'll call Cinderpath here per their program's disclosure terms, is a collaborative document platform that embeds a third-party identity verification widget inside an iframe during account setup, communicating results back to the parent page via postMessage.
Finding the Listener
Browser extension tooling makes this kind of client-side JavaScript auditing considerably faster than it used to be, and this is another place I leaned on AI assistance honestly rather than pretending I read every minified bundle by eye — I pulled Cinderpath's main application bundle, had it de-minified and formatted, and asked an LLM to locate every addEventListener("message", ...) call in the codebase along with a plain-language summary of what each handler did with the received data. That's a mechanical search-and-summarize task, not a judgment call, and it's the kind of thing that's tedious and error-prone to do manually across a large, heavily bundled production JavaScript file but fast and reliable to have surfaced automatically — provided, again, that I verified each flagged handler myself afterward rather than trusting the summary as the final word.
One handler stood out immediately once I read the actual surrounding code myself:
window.addEventListener("message", function(event) {
if (event.origin.indexOf("cinderpath-verify.com") !== -1) {
const data = JSON.parse(event.data);
if (data.verified === true) {
completeAccountVerification(data.userId, data.verificationToken);
}
}
});window.addEventListener("message", function(event) {
if (event.origin.indexOf("cinderpath-verify.com") !== -1) {
const data = JSON.parse(event.data);
if (data.verified === true) {
completeAccountVerification(data.userId, data.verificationToken);
}
}
});The check was there. Someone had clearly thought about origin validation — this wasn't a case of a missing check entirely, which is the more commonly discussed version of this bug class. The problem was how the check was written: indexOf("cinderpath-verify.com") !== -1 tests whether that substring appears anywhere in the origin string, not whether the origin is that domain. An origin like https://cinderpath-verify.com.attacker.com or even https://evil.com/cinderpath-verify.com would satisfy this check just as completely as the real, legitimate verification widget's actual origin would.
Building the Exploit Page
I registered a domain structured to pass the substring check — cinderpath-verify.com.attacker-owned-domain.com — and built a minimal page designed to open Cinderpath's account page in a popup window, then post a forged verification message to it:
<script>
const target = window.open("https://cinderpath.com/account");
setTimeout(() => {
target.postMessage(JSON.stringify({
verified: true,
userId: "VICTIM_USER_ID",
verificationToken: "FORGED_TOKEN_VALUE"
}), "*");
}, 3000);
</script><script>
const target = window.open("https://cinderpath.com/account");
setTimeout(() => {
target.postMessage(JSON.stringify({
verified: true,
userId: "VICTIM_USER_ID",
verificationToken: "FORGED_TOKEN_VALUE"
}), "*");
}, 3000);
</script>Hosted on the attacker-structured domain, loading this page would satisfy the indexOf check on Cinderpath's end, since my domain's origin string genuinely contained the substring cinderpath-verify.com even though it was in no meaningful sense the real verification service.
Confirming Impact
Testing against my own account, the forged message successfully triggered completeAccountVerification, which — based on what the function actually did once invoked — marked my account as identity-verified without any real verification having occurred, using a token value I'd simply made up rather than one genuinely issued by the real verification widget. The userId parameter in the forged message was also attacker-controlled, meaning the real severity wasn't limited to self-verification at all: with a known or guessable victim user ID, the same forged message could mark a different account as verified, bypassing whatever downstream trust or feature-gating Cinderpath's platform extended to verified accounts.
Scope
I tested this entirely against my own account's userId, never attempting the technique against any ID I hadn't confirmed was mine, and I didn't explore what additional privileges or features "verified" status unlocked beyond confirming the state change itself occurred.
The Report
Title: postMessage origin validation using substring matching allows arbitrary message forgery and verification bypass
Root cause: The message event handler validates the sender's origin using String.indexOf() to check for the presence of a trusted domain substring, rather than an exact match against the full expected origin. Any domain containing that substring anywhere in its own name — including attacker-registered domains structured specifically to include it — passes this check, allowing forged messages from untrusted origins to be processed as if they came from the legitimate verification widget.
Reproduction: The vulnerable handler code, the structured attacker domain satisfying the substring check, and confirmation that a forged message successfully triggered account verification using a self-controlled test account and a fabricated token value.
Impact: Any attacker can forge the verification completion message for any known or guessable user ID, bypassing identity verification entirely without ever interacting with the real verification service.
Fix recommendations:
- Replace the substring check with an exact match against the full expected origin string (
event.origin === "https://cinderpath-verify.com"), which cannot be satisfied by any structured attacker domain regardless of what substring it happens to contain - Never trust
data.verificationTokenor similar values without separately validating them server-side against a record of tokens genuinely issued by the verification service, since client-side origin checks alone should never be the sole gate on a security-relevant state change
What Happened After
Cinderpath fixed the origin check to use exact matching within three days and added server-side token validation as defense in depth. The report closed at $8,400, rated high severity.
Why This Pattern Is Worth Specifically Testing, and Where AI Helps Find It Faster
Substring-based origin checks are a genuinely common mistake, precisely because indexOf and includes feel like reasonable, simple tools for "does this contain the domain I trust" — right up until you think through what else can legally contain that same substring. Scanning a large client-side bundle for every message listener by hand is exactly the kind of volume problem that AI-assisted triage shortens meaningfully, surfacing candidates in minutes rather than hours of manual bundle-reading — but the actual vulnerability judgment, recognizing that indexOf isn't the same security property as exact match, and verifying that by hand against the real code, stays entirely a human skill no tool replaces.