november 2026 · 5 min

Every Message Listener Is an Endpoint

Parameter published a chain on October 28 that took over ChatGPT accounts through a shared link. The victim opens a /share/ URL (the most reliable variant needed one click on the preview; a later one opened it automatically), and code running as chatgpt.com lists their conversations and reads a private one. OpenAI fixed it on October 26 and rated it P1. No exploitation in the wild was reported.

The interesting part is what didn't fail. The attacker's HTML ran in ChatGPT's preview frame, which is sandboxed, and the sandbox held. What failed was a listener on the main page, installed by a dependency, that accepted messages from anyone.

The Chain

The shared conversation carried two things: an HTML preview and an interactive graph.

From there it's just authenticated API calls as the victim. Parameter's verdict on the last step: "CSP is valuable defense in depth, but it cannot make an unsafe rendering path safe." The Hacker News makes the same point: blocking the YouTube step alone would still leave an untrusted preview able to push HTML into the main site.

The Class

A message listener is an endpoint. It takes input from other windows, any of which can be hostile, and it runs with the full authority of your origin. It deserves the review you'd give a route handler, and when a dependency installs it, it's easy to miss that it exists at all.

That's the supply chain angle here, and there's no malicious package in it. MathLive wasn't compromised. Its defaults were the vulnerability, and any page that loads the component inherits them. That's what I mean when I say supply chain failures are a lack of agency: the dangerous trust is rarely a decision. It's a default that stays in place until someone goes looking.

There's an earlier MathLive bug with the same root. GHSA-fm7p-gw32-828p (June 2026, fixed in 0.110.0) was an XSS from unescaped \text{} and \mbox{} output, and createHTML is an identity function by default, so it never sanitized anything. Different bug, same lesson: a helper named like a sanitizer isn't one until you read it.

The Check

Find out who is listening on your top-level window. In Chrome DevTools, open the console on a page with all your components loaded:

getEventListeners(window).message

getEventListeners is a DevTools console utility, so this only works in the console, not in your code. You get every message handler registered with addEventListener on the window (not onmessage assignments, and not listeners in other frames), and you can click through to its source. Each one should check event.origin against an allowlist and check that event.source is the specific window it expects. If a handler does neither and comes from node_modules, that's an unauthenticated endpoint on your origin.

To find them before they ship:

grep -rlE "addEventListener\(\s*['\"]message['\"]" node_modules --include=*.js --include=*.mjs --include=*.cjs

Expect a lot of hits. Start with packages that touch iframes, editors or embedded widgets, and run the same grep over your built bundle, since that's what actually ships.

For MathLive specifically, 0.111.2 (October 31) defaults its keyboard, proxy and mathfield messages to same-origin validation, binds state-changing messages to the connected window, and stops cross-window settings from supplying custom layouts at all. Upgrade, and set originValidator explicitly instead of relying on the default.

The Tradeoff

Strict listeners break legitimate cross-window setups. A keyboard rendered in one frame driving a field in another is exactly the feature MathLive's "none" default made easy, and locking it down means naming your trusted origins by hand. I'd pay it.

The CSP lesson costs more. A host allowlist trusts every endpoint on that host, including the ones that reflect input. Moving to nonces with strict-dynamic instead of host lists is real work: every script tag you didn't nonce stops loading, including vendor snippets pasted into templates. But "we allow YouTube" was never a statement about YouTube's videos. It was a statement about every URL YouTube will answer.

The sandbox did its job. The listener next to it is the part you have to check yourself.

← all posts