- Book: Event-Driven Architecture Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Someone sends you a link to a shared ChatGPT conversation. You open it. In the final version of a chain the security firm Parameter built, opening the link was the only step before attacker JavaScript ran as chatgpt.com in your signed-in session. An earlier, more stable variant needed one click on Preview.
Parameter's writeup, published on October 28, says its team was able to take over anyone's account by sending them a link. OpenAI fixed it on October 26, eight days after the report, and no user action is needed. Parameter reports no real-world use, and its private-chat proof ran between separate accounts, not against real users.
The part that reaches past ChatGPT is the first link in the chain: a postMessage listener in MathLive, an open-source math input library, running with origin validation set to "none". That is the default the MathLive API reference documents. On October 31, MathLive 0.111.2 changed it.
How a share link became code on chatgpt.com
Parameter describes the chain step by step:
- The attacker publishes a normal conversation with an interactive graph and an HTML code block. The graph loads MathLive, which installs a
messagelistener on the main ChatGPT page. - The HTML Preview runs in a sandboxed frame on a separate origin. It can't read the parent page, but it can call
postMessageon it, and it sends messages of typemathlive#virtual-keyboard-message. - MathLive's controller used an origin policy of
none, so it accepted those messages from any origin, and it didn't require them to come from a window it had approved before. - The keyboard layout in the message was markup, and it went into the page through
innerHTML. In Parameter's words, "createHTMLacted as an identity or Trusted Types wrapper, not a sanitizer." - That markup created an iframe with
srcdocand nosandboxattribute. The main page created it, so it inherited thechatgpt.comidentity. - ChatGPT's Content Security Policy blocked inline scripts but allowed
www.youtube.com. A JSONP-stylecallbackparameter on YouTube's oEmbed endpoint supplied the script.
With that, the researchers listed a victim's recent conversations, fetched a private conversation through ChatGPT's authenticated API, and sent part of it to a server they controlled. Their summary: "Essentially, we could perform any action on behalf of the victim's ChatGPT account." They demonstrated the conversation read, not the rest.

Each piece looked reasonable on its own: a public share, a preview, a math keyboard, a frame, a CSP allowlist.
The timeline
From Parameter's timeline and MathLive's changelog:
| Date (2026) | What happened |
|---|---|
| Oct 18 | Parameter reports through OpenAI's bug bounty |
| Oct 21 | OpenAI reproduces it; severity goes from P2 to P1 |
| Oct 22–23 | One frontend fixed, CSP changes block part of the chain |
| Oct 26 | Both frontends reject the messages; $5,000 bounty |
| Oct 28 | Parameter publishes the writeup |
| Oct 31 | MathLive 0.111.2 defaults to same-origin validation |
The MathLive 0.111.2 entry doesn't mention ChatGPT, OpenAI or Parameter, and lists no CVE. I can't tell you whether the report prompted it. Parameter's writeup doesn't say it reported anything to MathLive's maintainers.
What MathLive 0.111.2 changes
The release has one section, Security. Its first line: "Virtual keyboard, proxy and mathfield messages now default to same-origin validation." The rest of the entry closes the other doors the chain used:
- Cross-window keyboard settings "cannot supply custom layouts or keycap overrides, even with origin validation disabled." Custom HTML layouts now get configured in the keyboard's own host window.
- "State-changing messages are bound to the connected mathfield window."
- Same-origin checks no longer treat two opaque
"null"origins as equal, or accept a wildcard sender origin. - "Cross-origin integrations must explicitly allow their trusted peer origins."
If your app ships MathLive, find out which version you have, then upgrade to 0.111.2 or later:
npm ls mathlive
npm install mathlive@latest
The API reference still lists "none" as the default for originValidator, so set the policy explicitly instead of relying on either version's default. The type is ((origin) => boolean) | "same-origin" | "none":
import 'mathlive';
// Explicit policy: no reliance on a default.
mathVirtualKeyboard.originValidator = 'same-origin';
If your keyboard really does talk across origins, pass a function that allows only the ones you trust:
const TRUSTED = new Set(['https://app.example.com']);
mathVirtualKeyboard.originValidator =
(origin) => TRUSTED.has(origin);
Check the listeners you wrote yourself
MathLive is one library. Any message listener on your page has the same exposure. MDN's postMessage page is blunt about it: "Failure to check the origin and possibly source properties enables cross-site scripting attacks." Start by finding them:
grep -rnE 'addEventListener\(.message|onmessage' src/
Parameter's lessons map onto a short checklist: check both event.origin and event.source before acting on a state-changing message, and never accept raw HTML across windows as configuration. A listener that does both looks like this:
const kbd = document.querySelector('#keyboard-frame');
const TRUSTED = 'https://keyboard.example.com';
const LAYOUTS = new Set(['numeric', 'alphabetic']);
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED) return;
if (event.source !== kbd.contentWindow) return;
const msg = event.data;
if (msg?.type !== 'set-layout') return;
// A name from a fixed list, never markup.
if (!LAYOUTS.has(msg.layout)) return;
applyLayout(msg.layout); // your own function
});

Check who sent the message, then check what it says. This chain started at a listener that checked neither.
Parameter's other lesson is about frames you create at runtime. Per MDN, a sandboxed frame without allow-same-origin "is treated as being from a special origin that always fails the same-origin policy." MDN also warns that combining allow-scripts and allow-same-origin on a same-origin document lets it remove its own sandbox:
const frame = document.createElement('iframe');
// No allow-same-origin: the frame gets an opaque origin.
frame.setAttribute('sandbox', 'allow-scripts');
frame.srcdoc = untrustedHtml;
document.body.append(frame);
And keep your expectations of CSP honest. ChatGPT had one, and an allowlisted host with a callback endpoint got the script through. Parameter puts it this way: "CSP is valuable defense in depth, but it cannot make an unsafe rendering path safe."
Preview panes are where these chains start
AI products render a lot of untrusted output: shared chats, model-written HTML, artifacts, previews. This chain existed "in the seams between public sharing, Preview, MathLive, frame creation, and CSP", and no single piece looked dangerous in review. A preview pane was also the way in for Noma's SneakPeek report on Claude Code Desktop, published in October.
If your app renders model output next to a third-party widget, list every message listener on that page and who can reach it. If npm ls mathlive prints something older than 0.111.2, start with that one.
If this was useful
This bug comes down to a handler that acted on a message without checking who sent it. Webhooks, queue consumers and event handlers raise the same question every day: who produced this, and should I act on it? The Event-Driven Architecture Pocket Guide is my short guide to systems that run on messages: sagas, CQRS, the outbox, and the traps nobody warns you about.
I also build Hermes IDE, a free, source-available IDE for running Claude Code, Codex and other coding agents side by side on macOS, Windows and Linux. Download it or star it on GitHub.

Top comments (0)