AI Agent Security

The ChatGPT account takeover came from a math keyboard, not the login flow

A public ChatGPT share ran attacker JavaScript as chatgpt.com through a MathLive postMessage listener and a YouTube JSONP endpoint. Any product that renders previews should audit for the same chain.

Published
October 31, 2026
Read
7 min
Published by
Samir Sengupta
Diagram of a ChatGPT share link chaining a MathLive postMessage listener and YouTube JSONP into account takeover
Note 118 / 118Daily note · Written from 1 source

How this was madeDrafted with Claude, Anthropic’s model, from 1 cited source, then checked against it in a second editing pass. Editorial policy

the short version

  • Parameter's write-up describes no flaw in OpenAI's login, SSO or OAuth flow. The ChatGPT account takeover was a cross-frame injection through a third-party math library, MathLive.
  • The HTML Preview sandbox held. The break came from a MathLive listener in the main page that accepted messages from any origin and rendered supplied HTML.
  • A CSP allowlist entry for YouTube was enough to run arbitrary script once attacker HTML reached the page, so CSP cannot be the only barrier.
  • Teams with sandboxed previews, chart widgets or connector UIs should audit every postMessage receiver that third-party libraries install in their top-level page.

Parameter's researchers achieved a ChatGPT account takeover with one link. They published an ordinary shared conversation that ran their JavaScript as chatgpt.com in the signed-in session of anyone who opened it. The write-up describes no flaw in OpenAI's login or SSO flow. Instead, a math library called MathLive, loaded by an interactive graph, accepted cross-frame messages from any origin. OpenAI classified the report P1 and fixed it on October 26, 2026.

The researchers confirmed the injected code could list the victim's conversations, fetch a private conversation never shared with the attacker, and send data from that conversation to an external server. They deliberately sent only a small piece of the response off-site, but the code could read all of it. The victim did not need to install an App, join a Project or authenticate through an attacker-controlled flow. In the automatic variant, opening the shared link was the only required action, and the proof alert appeared after roughly 4.5 seconds.

One Hacker News commenter read the write-up as "another classic OAuth implementation failure" involving a mismanaged state parameter or leaked tokens. A reply pointed out that the chain is an HTML injection via a postMessage call where the receiver did not check who sent the message. Nothing in Parameter's account involves the login path. The payload reused a session the victim had already established.

Why the ChatGPT account takeover bypassed login entirely

The exploit never touched credentials. The injected JavaScript ran as chatgpt.com, read the session information already available to the page, and used it to call the backend directly: GET /backend-api/conversations with a limit and offset to enumerate chats, then GET /backend-api/conversations/<id>?num_turns=2 for message content. Parameter describes this as direct authenticated API access as the victim, not scraping text visible on screen. Because the code ran inside the web application's security boundary, its reach was not limited to those endpoints; any action exposed through ChatGPT's authenticated frontend APIs sat in the same boundary.

That is the point for teams building LLM products. If your front end holds a session and renders model-authored or user-authored content, an injection in that page is equivalent to a session takeover regardless of how strong the login path is. Magic links, one-time codes and OAuth state validation do not help once attacker script executes under your origin.

How a graph and a Preview crossed the sandbox

ChatGPT renders an HTML code block through a Preview feature that runs inside a restricted frame on a different web address, so Preview code cannot read parent.document. That isolation held throughout. The break came from a second, unrelated feature. An interactive graph in the same conversation loaded MathLive, and loading it installed a listener in the main ChatGPT page for messages controlling its on-screen keyboard.

Browsers provide postMessage so two frames can exchange data even when they cannot read each other's pages. It is safe only if the receiver checks who sent the message. MathLive's top-level controller used an origin policy of none, meaning it accepted messages from any web address, and it did not require the update-setting message to come from a previously approved window. Anyone who could reach the listener and knew the message format could replace the layout.

javascript
top.postMessage({
  type: "mathlive#virtual-keyboard-message",
  action: "update-setting",
  layouts: [{
    label: "Compatibility",
    layers: [{ id: "compatibility", markup }]
  }]
}, "*");

top.postMessage({
  type: "mathlive#virtual-keyboard-message",
  action: "show",
  animate: false
}, "*");

The keyboard layout was allowed to contain raw HTML. When the keyboard opened, MathLive joined the supplied layout into an HTML string and assigned it to innerHTML in the real ChatGPT page. A helper named createHTML looked like it might sanitize the value; it did not, marking the string trusted and passing the HTML through unchanged.

How srcdoc and YouTube JSONP turned HTML into script

Script tags inserted via innerHTML do not run, so HTML injection alone was not code execution. The researchers used an iframe with the srcdoc attribute, which carries its HTML directly in an attribute. MathLive inserted that frame into the main document without a sandbox attribute. Because the main chatgpt.com page created the frame, the browser gave it the chatgpt.com web identity, and code inside it could access the parent.

ChatGPT's Content Security Policy still blocked inline scripts, but it allowed scripts from YouTube. YouTube's oEmbed endpoint supported a JSONP-style callback parameter and placed the callback into the response before returning a validation error. Ending the callback with ;void made the appended error object harmless, so the browser loaded the response as an allowed script and ran arbitrary JavaScript inside the unsandboxed frame.

javascript
// API callback
globalThis.__marker=31337;void({
  "error": {
    "code": 400,
    "message": "Invalid JSONP callback name: ..."
  }
});

Parameter is explicit that the YouTube endpoint was the final workaround for CSP, not the root cause. Blocking that one endpoint would have removed a path to script while leaving an untrusted Preview able to insert HTML into the main site. CSP, in their framing, should be a backup control rather than the only barrier after attacker-controlled HTML reaches the page.

Why one public share carried the whole chain

The first working version used a researcher-controlled ChatGPT App as the sender, which a victim would have had to connect first. Delivering the payload through a native HTML attachment worked because ChatGPT opened the attachment in the same sandboxed Preview. But when the conversation was exported as a public share, the attachment survived while the graph did not, and the graph was what installed the vulnerable MathLive listener.

The breakthrough was putting both pieces in one ordinary public conversation: a genuine graph to initialize MathLive in the main page, and a later assistant-authored HTML Preview to send the two messages. The resulting /share/<id> link preserved the complete chain for another user, deliverable by email, chat, a support ticket or a social post. The graph and Preview loaded independently, so the carrier retried the two messages for a limited period to handle the race, with a guard ensuring the private-conversation proof ran only once.

What OpenAI changed, and what the bounty signals

OpenAI reproduced both the JavaScript execution and the private-conversation disclosure. Parameter submitted the report on October 18; severity was raised from an initial P2 triage to P1 on October 21. The fix rolled out across two versions of the web frontend; on October 22 OpenAI reported one fixed while the second was still being updated, and on October 23 CSP changes blocked part of the chain while one frontend update remained. On October 26, OpenAI confirmed both frontends rejected the cross-frame MathLive messages and marked the issue resolved. The first confirmation against production code dates to October 13.

Commenters on Hacker News focused on the payout. One cited a total bounty of $5,000 and called it low for a P1 that can take over accounts by link. Another pointed to a known position that "server-side vulnerabilities are worth very little, because they quickly get patched." Others worried that pricing a link-click account takeover this low discourages researchers from reporting the next one. The sources do not resolve that debate, and OpenAI's reasoning on the amount is not given.

What to check in your own preview, connector and chart flows

Parameter's defensive lessons translate to any LLM product that renders model-authored HTML, embeds charts or ships a connector UI. Every individual ChatGPT feature was reasonable; the bug appeared only when sandboxed Preview, a graph that loads MathLive, cross-frame messaging, a CSP allowlist for a major media provider, and public sharing combined in one page.

  • Audit every postMessage receiver, including ones installed by third-party libraries like MathLive, and verify both the sender's origin and the exact window granted permission rather than using an origin policy of none.
  • Never let cross-window settings carry raw HTML; keyboard layouts, themes and config should be data, not markup assigned to innerHTML.
  • Keep dynamically created documents sandboxed when their content can be influenced by an untrusted frame, so an srcdoc iframe cannot inherit your authenticated origin.
  • Treat CSP as a backup control and assume any allowlisted third party, such as a JSONP endpoint, can be turned into a script source once attacker HTML lands.

Some Hacker News commenters reduced the lesson to "don't click links," but that misses where the risk sits. Commenters noted that some email security scanners auto-confirm links on GET, and that legitimate products routinely send one-time-code and "click here" emails, so user caution is not a control you can rely on. The defensible boundary is inside your own front end: assume model-authored and shared content is hostile, and make sure no component in your main page will act on its instructions.

Questions this raises

Did the ChatGPT account takeover exploit a flaw in OpenAI's login or OAuth flow?

No. The write-up describes no flaw in login or SSO. Attacker JavaScript ran as chatgpt.com and reused the session the victim had already established to call the backend APIs directly.

How did a sandboxed HTML Preview reach the main ChatGPT page?

An interactive graph in the same conversation loaded MathLive, which installed a postMessage listener in the main page that accepted messages from any origin. The Preview sent a keyboard layout containing raw HTML, which MathLive inserted via innerHTML, including an unsandboxed srcdoc iframe that inherited the chatgpt.com identity.

When did OpenAI fix the vulnerability?

OpenAI reproduced the issue, rolled fixes across two web frontends, and added CSP changes on October 23. On October 26, 2026 it confirmed both frontends rejected the cross-frame MathLive messages and marked the issue resolved.

Spotted an error? Email samir.s.sengupta@gmail.com and I will correct it and date the change at the top of the note. How these notes are researched, drafted and checked is in the editorial policy.

More notes

Building something on this?

I ship production LLM, RAG and agentic systems for a living - the infrastructure behind the things these notes are about. Open to roles, contract work and research collaboration.