DEV Community

Cover image for A public ChatGPT share could run code as you. I checked the two parts that made it possible.
frank chu
frank chu

Posted on

A public ChatGPT share could run code as you. I checked the two parts that made it possible.

A public ChatGPT share could run code as you. I checked the two parts that made it possible.

Parameter's disclosure reads like a magic trick with the method explained: an ordinary public ChatGPT share link, opened by a signed-in victim, could run JavaScript as chatgpt.com — enumerate the victim's conversations, read a private one, and send data to an attacker server. OpenAI fixed it on October 26 and paid a $5,000 bounty. I did not reproduce the full chain against a live ChatGPT account. The chain is fixed, and I don't have two spare ChatGPT accounts lying around for account-takeover theater. What I could do is verify the two load-bearing components that don't need a ChatGPT account at all: the message listener in the public MathLive package, and the YouTube endpoint that smuggled the payload past Content Security Policy. Both checked out, exactly as described.

The chain, in one paragraph

ChatGPT's HTML Preview runs untrusted code in a sandboxed frame on a different origin — that part worked. But the same conversation could also contain a graph, and the graph loaded MathLive, whose virtual-keyboard listener lived in the main chatgpt.com page. The sandboxed Preview could postMessage to that listener. The listener accepted messages from any origin, let the message replace a keyboard layout with raw HTML, and rendered that layout with innerHTML in the main page. An iframe with srcdoc inside that HTML inherited the chatgpt.com origin, and a YouTube JSONP quirk supplied the actual script execution past CSP. Preview was the untrusted sender; the graph was the trusted receiver. Neither feature was broken on its own.

Check 1: MathLive really does default to accepting messages from anyone

I pulled the current MathLive release from the npm registry — version 0.111.1, fetched October 31 — and read the virtual-keyboard code in mathlive.mjs. The origin check is one function:

function validateOrigin(origin, originValidator) {
  if (origin === "*" || originValidator === "none") return true;
  if (originValidator === "same-origin")
    return !window.origin || origin === window.origin;
  if (typeof originValidator === "function") return originValidator(origin);
  return false;
}
Enter fullscreen mode Exit fullscreen mode

And the default validator, in three places, is "none":

proxy constructor:            this.originValidator = "none";
main-page controller:         this.originValidator = "none";
default mathfield options:    originValidator: "none",
Enter fullscreen mode Exit fullscreen mode

Both sides of the conversation — the proxy inside the frame and the controller in the main page — call validateOrigin before handling a mathlive#virtual-keyboard-message, and with the default both calls return true for any origin. The disclosure's phrase "origin policy of none" is not a paraphrase. It is the literal default in the shipped package.

Then the layout path. When the controller handles an update-setting message with layouts, it stores them, and rendering puts the layout markup into the page:

element.innerHTML = globalThis.MathfieldElement.createHTML(markup);
Enter fullscreen mode Exit fullscreen mode

createHTML looks like it might sanitize. It doesn't — it marks the string trusted and passes it through unchanged, which is exactly what the disclosure says. So the two messages from the writeup — update-setting with a crafted layout, then show — are, at the library level, a real path from "any frame can post a message" to "arbitrary HTML in the host page." The only thing standing between that and script execution in the original chain was CSP, which brings us to check 2.

Check 2: YouTube's oEmbed endpoint still echoes your callback before its error

The disclosure's CSP bypass used YouTube's oEmbed endpoint, which ChatGPT's CSP allowed as a script source. The endpoint rejected the crafted callback name — but put the callback into the response first. I fetched the exact URL from the writeup today:

$ GET https://www.youtube.com/oembed?callback=globalThis.__marker=31337;void
status:       200
content-type: text/javascript; charset=UTF-8
body:
  // API callback
  globalThis.__marker=31337;void({
    "error": {
      "code": 400,
      "message": "Invalid JSONP callback name: 'globalThis.__marker=31337;void'; ..."
    }
  });
Enter fullscreen mode Exit fullscreen mode

The ;void at the end of the callback swallows the error object that follows, so the whole response is valid JavaScript that runs the attacker's statement. This endpoint still behaves this way on October 31, after the ChatGPT fix — because the endpoint itself was never the bug. It was just a script source ChatGPT's CSP happened to allow. Blocking YouTube would have removed one path while leaving the untrusted frame able to inject HTML into the host page. The disclosure says this explicitly, and it's the most important sentence in the whole writeup: CSP is a backup control, not the barrier.

The timeline, and what "fixed" means here

Oct 13  MathLive message-to-HTML path confirmed against production ChatGPT
Oct 14  full chain reproduced in the UI, alert upgraded to arbitrary JS
Oct 16  public-share delivery confirmed between separate accounts
Oct 17  automatic Preview path reproduced after hard refresh (~4.5s to alert)
Oct 18  reported to OpenAI's bug bounty program
Oct 21  OpenAI reproduced it; severity raised P2 -> P1
Oct 22  one web frontend fixed, second still updating
Oct 26  both frontends reject the reported messages; resolved, $5,000 bounty
Enter fullscreen mode Exit fullscreen mode

Report to fix: 8 days. First confirmation to fix: 13 days. That's a fast, clean turnaround, and the disclosure credits OpenAI for reproducing both the execution and the private-conversation proof.

But notice what the fix was: ChatGPT's frontends now reject the cross-frame MathLive messages. The library I downloaded today still ships originValidator: "none" as its default. The vulnerability was never in MathLive alone — a math keyboard that accepts layout updates by postMessage is a reasonable feature for an app that embeds it intentionally. The vulnerability was the combination: an embedder that put a listener with the permissive default into a page where an untrusted frame could reach it, next to a preview feature that gave that frame a sender.

That's the lesson worth carrying out of this one. Audit the defaults of the components you embed, not just your own code. A postMessage listener with an origin policy of "none" is a loaded gun that every frame on the page can fire, and the frame that fires it might be one your own product created for a completely different feature. The Preview sandbox did its job. The keyboard listener undid it.

Sources: Parameter, "Do the Math: How We Took Over ChatGPT Accounts" (October 2026), including the two postMessage payloads and the YouTube oEmbed URL; MathLive 0.111.1 from the npm registry, mathlive.mjs (validateOrigin, proxy/controller defaults, innerHTML rendering), fetched October 31, 2026; my own live fetch of the YouTube oEmbed endpoint, October 31, 2026. I did not access any live ChatGPT account or reproduce the chain against chatgpt.com; the fix has been in place since October 26.

Top comments (0)