Back to all posts
Published on · by Renaud Deraison

The registry passed notes between sandboxes

Check Point published research on September 8 showing that two ChatGPT code-execution containers, running under two different accounts with no network path between them, could pass data to each other through the internal JFrog Artifactory instance that served them packages. Nothing on the front door broke. The reader credential those containers held could also write, so a metadata field on a stored package worked as a mailbox. Bromure Agentic Coding hands the guest no account on the mirror, and lets you write read-only into a rule the wire enforces.

Isolation held, the connectors behaved as documented, and the service in the middle was installed for security in the first place. The victim's mail still reached a stranger's account, through a metadata field on a stored package.

In June, researchers at Check Point wrote a property onto a file in a package repository. The property was called chatgpt_test_ts and its value was a timestamp. They wrote it from inside the sandbox that runs code for a ChatGPT conversation. Then they opened a conversation under a different account, in its own container, and read the same property back. The value came through intact.

That timestamp is the whole finding. Check Point published it on September 8 under a title that beats any summary: The Shared Clipboard Inside the Sandbox. OpenAI had retired the service by then. The researchers disclosed in late June and were told the internal Artifactory instance had been decommissioned, so there is no patch to apply and nothing for a user to do. The shape of it stays.

The service in the middle

A ChatGPT conversation that runs code gets its own container, isolated from the containers of other conversations. That part worked. A container running Python also needs to pip install, and a careful platform team will not let a sandbox reach pypi.org on its own. So the sandbox gets a controlled intermediary: an internal JFrog Artifactory instance that fronts the public registries and serves vetted packages inward.

This is good design. A bank puts the same thing in front of npm, and a regulated healthcare shop puts it in front of PyPI. The mirror exists to reduce the sandbox's exposure, and it does that.

It also gives each container an account on the mirror.

Artifactory's REST API includes an Item Management group, reachable at /api/storage/{repoKey}/{itemPath}, that lets a client attach arbitrary named string properties to a stored item and read them back: Set Item Properties and Get Storage Item Information. Setting a property requires the Annotate permission. Reading one does not. Check Point found that the credential issued to the sandbox containers, meant as a reader, carried both. As Cybersecurity News summarized it, "reader credentials handed to the containers had enough permission to both write and read these properties", and that "those properties were not isolated per account."

Property values take text, or Base64 for anything binary, and a payload too big for one property splits across several and reassembles on the other side. Two containers belonging to unrelated customers, with no network path between them, now hold a bidirectional channel with no bandwidth ceiling worth mentioning.

two accounts, no path between them, one service they both needattacker’s accountcode-execution containerreader credential + Annotatevictim’s accountcode-execution containerreader credential + Annotateisolation holdsno network, no shared diskinternal JFrog Artifactory — the vetted package mirror/api/storage/{repoKey}/{itemPath}one stored package, one named propertychatgpt_test_ts = “…” · Base64, chunked if largeproperties are not partitioned by accountSet Item PropertiesGet Storage Item Info
The covert channel. Two ChatGPT conversations under different accounts each get an isolated code-execution container, with no network path between them. Both containers must reach the same internal Artifactory instance to install packages, and the reader credential they were issued also carried the Annotate permission. Container A writes a named property onto a stored item; container B reads it back. Properties were not partitioned by account, so the metadata plane of the package mirror worked as a shared mailbox.

Two conversations in the same turn

The attacker's instructions reach the victim by ordinary routes: a pasted prompt, a shared conversation link, or a custom GPT whose builder instructions the user does not see. The victim then sends a normal message, and that is the whole trigger.

In Thinking mode, the model processed two independent streams of work in one turn. Check Point's proof of concept had the victim ask a cooking question and receive a correct answer to it, while alongside that answer the model read the victim's connected Gmail account and wrote what it found into an Artifactory property for the attacker's container to collect. Pedro Drimel Neto, who leads Check Point's malware analyst team, told The Register: "A crafted instruction could make ChatGPT process a second stream of tasks alongside the visible conversation." Gmail was the demonstration. Google Drive, Microsoft Teams and GitHub sat within the same reach.

The victim saw one signal, a small "Talked to Gmail" label, and it appeared after the fact. Connectors default to the Important actions setting, which approves low-risk reads without asking; the stricter Always ask would have produced a prompt. That setting classifies a mail read as low risk without asking who requested it.

Check Point's description of the model: it "can therefore act as a coerced insider: not malicious, but persuaded to act on the attacker's behalf."

The part that generalizes

Strip out ChatGPT and the story is about a coding agent, because the structure is identical.

An agent sandbox has to install packages. Letting it reach the public registries unmediated is the thing you were avoiding, so you put a mirror in front: Artifactory, Nexus, a private npm proxy, whatever your organization standardized on. The sandbox now authenticates to that mirror, so it holds a credential, and somebody decided what that credential could do long before this agent existed. Package registries accept uploads, tags, annotations and retention rules, which makes Annotate a small permission sitting on a wide surface.

Ask two things of your own setup, then, rather than asking whether the sandbox is isolated. Name what it holds an account on. Then name which verbs work on that account.

Inside a Bromure workspace

Bromure Agentic Coding runs each coding agent in a hardware-virtualized Linux VM on your Mac, and keeps the real secrets on the host side of that line. Walk Check Point's three ingredients through a workspace.

A shared service. There is none. Each workspace maps to one VM, with its own system disk, its own home image, its own IP, and its own MITM proxy listener on the host. Two workspaces share no VM, and the manual's description of a pair of them is that they "never share a byte." No fleet-wide mirror sits underneath the workspaces with a metadata plane both can write to. What sits underneath a workspace is a process on your own Mac, one per workspace.

A credential on the mirror. The agent holds none. Package installs in a Bromure workspace leave over a virtio socket to the host proxy, and the proxy talks to the registry. Bromure keeps reputation-service API keys host-side and exports them into no VM. The Depi path is the sharpest case: the proxy attaches your registry key as an Authorization: Bearer header on the host and strips whatever Authorization header the guest sent on the way past. No reader credential exists in the guest, so none can carry a verb too many.

A write verb that outlived its reason. Here you get to write the constraint down. A workspace's Guardrails pane carries an ordered egress firewall, and rules for the web protocol take a Methods column. Under allow, the methods form an allowlist, and read-only is shorthand for GET, HEAD and OPTIONS. The manual says why the column exists: the method dimension "is what lets a workspace read an API it is not allowed to write", at the wire, regardless of what tool inside the VM makes the request. Two layers enforce it. The virtual network switch matches each flow by destination IP and DNS-snooped hostname, and the proxy matches again by TLS server name and by individual HTTP method, so a compromised agent has no route around either. Edits reach running sessions without a reboot, and each verdict lands as a Firewall row in the Security Timeline.

the decision made upstreamsandbox containerreader token + Annotatescope set once, long agoGET + PUTshared mirror — one instance, every tenantpackages on the artifact plane, mailboxes on the metadata planewhat the token may do was never a per-session questionthe decision made on the wire, per workspaceworkspace VMno registry credentialfakes only, brm-…no other route to the networkvsock 8443host proxy — one per workspace, on your Macstrip guest Authorization · attach real keyallow web registry.npmjs.org read-onlymatched by TLS server name and HTTP method, every request
The same requirement, two placements of the decision. Above: the sandbox authenticates to a shared mirror, and what it may do there was decided upstream, once, in a permission grant that outlived the person who made it. Below: the guest holds no registry credential and no route to the network except a virtio socket to a per-workspace host proxy, where the key is attached, the guest's own Authorization header is stripped, and an ordered rule decides which HTTP methods are allowed to reach the host at all.

The second stream, and the way back out

The attacker's second task stream arrived as content the model read. In a Bromure workspace, whatever an agent ingests and streams back to the model (file contents, web pages, command output, anything a tool returns) travels as tool_result spans through the same host proxy. The source-code detector scores those spans with a local PromptGuard classifier before the model acts on them, then logs the hit, pauses for your decision, or blocks the request. The detector ships switched off and pulls a 298 MB model the first time you enable it, so enable it before the session you need it in.

The unapproved connector read has an inverse here. A Bromure workspace's MCP bearer tokens appear inside the VM as brm-mcp_ placeholders. The real token stays out of the guest, and the proxy substitutes it on the wire toward the one host that token was minted for. Switch on Ask before use for a credential and its first use in a session pauses for a host-side dialog offering a 5-minute, 1-hour, or rest-of-session grant. That is the prompt "Important actions" declined to show, per credential, on the machine you are sitting at.

Should something in the VM carry a placeholder somewhere it does not belong, the proxy refuses the request without forwarding a byte, pauses the VM, and raises an alert naming the token preview, the credential, the host it was minted for and the host it was heading to. Under that tripwire, tracing at Activity only writes one metadata row per request: host, method, path, status, bytes, which credentials the proxy swapped, and a leak warning for any bearer-style token Bromure did not mint. Run bromure-cli trace hostnames after a session and each distinct host the agent contacted prints with a count.

If you run a mirror for your agents

Check the identity your sandboxes authenticate with, ahead of the network path between them. On Artifactory, look for Annotate on a token you think of as a reader, and for a permission target broader than the repositories the sandbox installs from. The property API is one writable metadata plane; retention rules, tags and build info are others. A reader that can annotate can post.

The rule that answers it in one line

In the workspace's Guardrails pane, add an egress rule for the registry with the web protocol and read-only in the Methods column, then set Unmatched traffic to Deny and list what the work needs. The pf-format disclosure at the bottom of the pane prints what you built: allow web registry.npmjs.org read-only, allow web api.github.com, default deny. It applies to running sessions without a restart.

A shared service makes tenants neighbours

The instinct after a story like this one is to check the walls. The walls were fine. Two containers under two accounts had no route to each other, and Check Point found none. They found a third party both containers were entitled to talk to, and used it the way people have used shared drives and DNS TXT records for thirty years.

Check Point states the lesson in the paper: "a shared internal service, meant purely as infrastructure, can become an unintended communication layer across environments that were supposed to remain isolated." Infrastructure you install for security is still infrastructure, and its tenants sit one hop apart. Shrink the set of things a sandbox holds an account on, and move the decision about what those accounts may do onto each request, out of a provisioning ticket somebody closed two years ago.

An agent sandbox worth running will have a controlled path to the packages it needs, because the alternative is worse. Treat that path as a relationship with permissions attached, and treat those permissions as your perimeter. Decide them somewhere you can still see them. Install Bromure Agentic Coding and give your agent nothing to sign in with.