← Back to all posts
Published on · by Renaud Deraison

The last step was a prompt

On September 18 Hacktron published the chain it used to reach OpenAI's private monorepo. It begins with a picture uploaded to a public forum and a heap overflow in an image decoder whose upstream fix carried no CVE. It ends with the researchers sending a prompt to an employee's Codex account and the agent opening the pull request. Nothing was injected and nothing malfunctioned; the agent did what whoever held the session asked. Bromure Agentic Coding decides that write on your Mac, outside the session.

The last step in this chain needed no exploit. The researchers were holding an employee's coding agent, so they asked it to open a pull request in the private monorepo, and it opened one.

At six in the morning UTC on July 25, three researchers at Hacktron had code execution on OpenAI's public community forum. By early afternoon a pull request numbered 1186742 was sitting in openai/openai, the private monorepo, and they had written no code to put it there. They asked an agent, and the agent filed it.

Hacktron published the chain on September 18, and it repays reading link by link, because every link is ordinary. The researchers are Harsh Jaiswal, Mohan Pedhapati and Rahul Maini. The Hacker News covered it the following day.

A picture, a conversion, and a parser

The way in was the forum's image upload. Discourse checks uploaded images with a Ruby library called FastImage, which reads enough of a file's header to work out its dimensions and format. FastImage does not understand HEIF, the container an iPhone photo arrives in, so Discourse handed those files to ImageMagick's magick command for conversion. ImageMagick decodes HEIC with libheif, which is C.

That is the whole entry condition. A format the safe checker did not recognise went to a converter that called a parser written in a language where a malformed length field is a memory-safety problem. Hacktron found a heap buffer overflow in HEIC decoding and got out-of-bounds read and write primitives from it.

The paperwork is worse than the bug. Upstream had fixed that overflow the previous year and never labelled the commit a security fix. It got no CVE, no advisory, and no changelog line a dependency scanner could read, so Debian never backported it, and Discourse's Docker image shipped libheif 1.19.7 on Debian 12 with the bug intact. No scanner anywhere had grounds to flag that version. Debian issued DSA-6417-1 later and the issue now carries CVE-2026-32882, assigned after somebody exploited it.

You cannot scan for a bug nobody filed. What you can decide in advance is what the parser is standing in when it falls over.

One upload to one pull requestthe parsea HEIC uploadposted to a public forumno account privileges neededFastImagethe safe header checkdoes not support HEIFmagickconverts it insteadImageMagick calls libheiflibheif 1.19.7heap overflow, OOB read/writefixed upstream, no CVE, no backportthe pivotcode executioncommunity.openai.com06:00 UTC, July 25an SSO flawa forum session becomesaccount takeover, no interactionan employee's Codexconnected to the GitHub orgalready authorised to writeno exploita promptPR #1186742private monorepo, openai/openai
Every link in the chain is ordinary. FastImage does not support HEIF, so the file goes to ImageMagick, which calls libheif, which held a heap overflow whose upstream fix carried no CVE and never reached Debian. A separate SSO flaw turned code execution on the forum into account access, and account access into a pull request in the private monorepo. Only the final step needed no exploit: the researchers sent a prompt to a coding agent that was already authorised to push.

The exploit took an evening

Hacktron started looking at the upload pipeline on July 23. On July 24 they went after the overflow with Claude Opus 4.8, which by their account struggled across several sessions to produce anything that worked against address space layout randomisation, the defence that shuffles a process's memory map so an attacker cannot predict where anything lives. Turning a heap overflow into reliable execution against ASLR is the hard part, and it is where this kind of research stalls for a week.

Anthropic released Opus 5 that evening. Hacktron started a fresh session, and three hours later had a working ARM64 exploit running against a local Mac. They asked the model to port it to the x86-64 and jemalloc configuration Discourse runs on, and confirmed local code execution through an image upload at six the next morning. Then they put Claude in an autonomous loop against their own Discourse Cloud instance. By ten it had code execution there too, and read /etc/hosts to prove it.

They are careful about what that means, and the qualifier belongs in any reading of it:

This was not fully autonomous hacking. Human expertise remained necessary to direct sessions, validate results and manage disclosure.

Fair. Three skilled researchers pointed the thing, and their skill did not change overnight. The bug did not change either. Between those two sessions the only new variable was the model, and it took a piece of exploitation work from several failed attempts down to three hours. The campaign this belonged to cost Hacktron under $3,000 in tokens and a few hours of human time.

That number is the planning input. A bug that never got a CVE, sitting in a transitive dependency of a file converter, now converts into a reliable primitive for the price of a laptop. Waiting for the advisory is a hope, and nobody out-runs that clock by reading changelogs faster.

The last step was a prompt

Code execution on the forum gave Hacktron nothing OpenAI cared about. A second problem did that. OpenAI had wired its single sign-on so a compromised forum session became a no-interaction takeover of the ChatGPT and Codex account of any employee who used the forum. In Hacktron's words, the SSO issue is what turned a forum compromise into access to ChatGPT and Codex.

An employee's Codex was connected to the company's GitHub organisation, so the researchers did the obvious thing. They sent that account a prompt asking it to open a pull request in the internal monorepo, and PR 1186742 appeared in openai/openai. Then they stopped, without reading source, and filed the report.

Read that step again. Someone holding a valid session asked the agent to do an ordinary thing, and the agent did it with the write access it already had. No injection, no jailbreak, no malformed input. Every control anyone had built sat behind that session: the login, the org membership, the repository permissions. Once an attacker held the session, they held all of it.

A coding agent is a standing grant of write access to your repositories, attached to a conversation. Whoever reaches the conversation inherits the grant. That holds for Codex in a browser, and it holds for Claude Code, Codex CLI or Gemini CLI on a laptop, where the agent carries a real ghp_ token in ~/.git-credentials and a real GH_TOKEN in its environment. On a laptop the ways in multiply: a poisoned file in a repository, an issue comment, a package postinstall script, a hijacked terminal.

OpenAI patched the forum in about fourteen hours. The report went in that morning and OpenAI confirmed the fix at 22:49 UTC the same day. Discourse shipped its own fix over the weekend and published advisory GHSA-vhm9-85gw-x335 on July 28, and the bounty came to $6,500. The piece of this shape that sits on your own desk is the agent.

Where the write gets decided

Bromure Agentic Coding keeps the answer to "who is allowed to push" on your Mac, outside the agent's session, because the session is what an attacker is trying to reach. Two things happen there.

First, the agent's environment holds no token to inherit. The workspace VM gets a placeholder: ghp_ plus 36 characters, shaped so gh and git accept it without complaint, derived from your real token with HKDF-SHA256 and a per-install salt so it never appears to rotate between sessions. Your real token lives encrypted on the host, and the host proxy substitutes it onto the wire after the request has left the VM, only when the destination matches the host that token was minted for. As the manual puts it: no file, environment variable, or process inside the VM ever contains a real API key, OAuth token, AWS secret, or SSH private key. Carry that placeholder somewhere it does not belong and the proxy answers HTTP 451 without forwarding a byte, pauses the VM, and writes a red row into the Security Timeline.

The second is the one this story asks for. Every git forge credential carries a write policy, set per workspace in the Guardrails pane, and new workspaces default to Prompt before write. Reads pass without comment. A git push arrives on the wire as git-receive-pack and counts as a write, and so does a POST to the pull requests API. When one arrives, the proxy holds the request and opens a dialog on your Mac carrying the literal operation rather than a summary of it: the method and the path. You get Allow for 15 minutes, Allow once, Allow for the rest of the session, or Don't allow. A refusal reaches the agent as an ordinary API error it can report, not a hung connection.

The pane's own description of where this runs is the sentence that matters: enforced in the proxy, so a compromised agent in the VM cannot bypass it.

Whoever is driving the agent, you, a prompt injection buried in a fixture file, or somebody holding a session they took from you, the write leaves through the same proxy and gets classified the same way. The dialog on your Mac is the authorisation, and an attacker holding your session does not have your Mac.

An agent on the laptopthe agent sessionreachable by you, by an injection, by anyonewho takes the sessionreal credentials, in reachGH_TOKEN · ~/.git-credentialsreadable by every process on the machinePOST /repos/org/repo/pulls → GitHubthe session carries the authorityAn agent in a Bromure workspacethe agent session, inside the VMghp_ + 36 characters, a placeholdernothing real to inheritthe switch diverts :80 and :443the host proxy classifies itPOST /repos/org/repo/pulls = writegit-receive-pack counts tooa dialog on your Mac, carrying the literal call15 minutes · once · the session · don't allowthe Mac carries the authority
On a developer laptop the agent holds a real token, so a prompt suffices and the session carries the authority. In a Bromure workspace the VM holds a placeholder, the host proxy substitutes the real token on the way out, and the guardrail classifies the write before it reaches GitHub. Prompt before write, the default for new workspaces, opens a dialog on your Mac carrying the literal method and path. Whoever is holding the session, they are not holding the Mac.

The parser problem comes to your desk too

The other half of the chain generalises too, and few people have priced it in.

A coding agent spends its day reading files it did not write: test fixtures that came with the repository, an image attached to a bug report, a screenshot you pasted into the conversation, a PDF of a spec, a tarball from a release page, a font, a video the front end is supposed to play. Handling those means shelling out to magick, ffmpeg, pdftotext or unzip, or to an image library bound into Python, and each of those is a C or C++ parser eating bytes somebody else chose. That is the condition that let a forum upload reach libheif. An agent hits it more often than you would, because it never hesitates before opening something.

Detection has no answer here. The libheif bug carried no CVE at the moment it mattered, so no scanner, allowlist or version pin would have said a word. You get to choose the blast radius, and that is all.

In a Bromure workspace that decoder runs inside an Ubuntu guest under Apple's Virtualization.framework, a hypervisor boundary rather than a process sandbox, in a guest Bromure treats as untrusted by construction. A heap overflow in an image parser buys an attacker a shell in a Linux VM whose credentials are placeholders and whose route to the internet is an ordered ruleset on the host. The virtual switch matches that ruleset on destination IP and DNS-snooped hostname across TCP and UDP, and the proxy matches it again on TLS server name and HTTP method. Set unmatched traffic to Deny and the shell has an allowlist and nothing else. The switch diverts :80 and :443 into the proxy itself, so there is no proxy variable for the shell to unset. Rule edits reach running sessions without a reboot, and every verdict lands as a Firewall row in the Security Timeline.

Assume the parser falls over

This bug had no CVE at the moment it counted. Plan for the decoder to lose, and decide ahead of time what it is standing in when it does.

A session is not an authorisation

Anyone who reaches the conversation inherits the agent's write access. Put the decision for a push somewhere the conversation cannot reach.

Leave nothing in the VM to steal

A placeholder that tools accept is worth nothing upstream, and worth less than nothing to an attacker who carries it somewhere else.

Price the exploit

Three hours and a few thousand dollars in tokens. Reading advisories faster does not answer that number.

Hacktron's write-up is good research, and honest about how much of it a human still had to steer. The ending is the part that will still be true next year. Every defence in the chain sat behind an identity, and once a session carrying that identity changed hands, none of them had an opinion any more. Someone asked the agent, and the agent complied.

If your agent can push, ask where the permission to push lives, and whether an attacker who took everything the agent has would take that too. On a laptop they would. Move that one decision off the machine the agent runs on, and the last step of this chain has somewhere to stop.