Back to all posts
Published on · by Renaud Deraison

The token leaked from the process next door

Researchers pulled a JWT out of a neighbouring Cloudflare Worker at 12 bits a second, with no sandbox escape and no memory-safety bug. The isolate boundary held. The secret crossed it anyway, through the hardware underneath. Cloud platforms now offer that same boundary for running AI-agent-written code. Bromure Agentic Coding reaches one layer lower, and keeps your credentials out of the box entirely.

One tenant's JavaScript asked the CPU a few million shaped questions and read another tenant's authentication token out of the answers. It stayed inside the boundary the platform drew for it, running plain JavaScript, and the boundary did its job the whole time.

On August 17 a team of researchers posted Remote-Timer-as-a-Service, and on the 19th Cloudflare published its own account of the work. They pulled a JSON Web Token, the signed string a service accepts as proof of who you are, out of a co-located Cloudflare Worker in production, at up to 12 bits per second with 99.16% accuracy. The previous public result against the same platform, from 2021, managed 2 bits per minute. At 360 times the rate you cross the line between a curiosity and a stolen credential.

Cloudflare has fixed it, found no evidence anyone else used it in three years, and says no customer data was accessed. Read past that and look at what the attack never had to do.

Nothing had to break

Cloudflare Workers runs a lot of tenants on a little hardware, and it does that by skipping the thing that usually separates them. In Cloudflare's own words, Workers "runs code from multiple tenants in separate V8 isolates within the same operating-system process." An isolate is a JavaScript-level container: its own heap, its own globals, no shared memory, no threads. It boots in milliseconds, which is the entire point, because process isolation costs you a cold start on every request.

The attacker needs code running in one isolate. Getting the victim into the same process is the elegant part: the attacker's Worker calls the victim. As the researchers put it, invoking the victim from the attacker's script with fetch("https://victim.example") "will in most cases cause the scheduler to spin up an instance of the victim worker in the exact same process." You never wait for co-location, you summon it.

From there the attack is Spectre. Mistrain a branch predictor so the CPU speculatively executes a read it should never have made, then measure the cache to learn what it read. Cloudflare had already spent years making that hard: no SharedArrayBuffer, no threads, timers frozen and coarsened so you cannot measure anything as small as a cache hit, and Dynamic Process Isolation (DyPrIs), which watches hardware performance counters and moves anything that smells like Spectre into its own process.

The researchers went around each one.

The timer came from outside. A WebSocket to a Durable Object, or to an ordinary timestamp server on the internet, gives you sub-millisecond resolution from a platform that refuses to give you a clock. Then they amplified the signal instead of resolving it: the L1 cache's tree-PLRU replacement policy follows binary-tree pointers, so one cache event can be turned into a long run of hits or a long run of misses. "The timing of a single cache event can be arbitrarily amplified," they write. A blurry clock is fine if you make the thing you are timing enormous.

DyPrIs missed it twice over. It isolates a script after the invocation finishes, and a Durable Object kept alive by keep-alive messages runs for hours, up to a day, so the exploit completes inside a single invocation that never ends. It also normalises branch mispredictions by iTLB accesses, while, in Cloudflare's words, "our remote timer is one large I/O loop, and that WebSocket traffic inflates iTLB activity." The researchers grew their own denominator until the alarm read normal.

The rest is plumbing: a speculative type-confusion gadget to leak the isolate's root address, 64-bit pointer confusion on a typed array's backing store to turn that into an arbitrary read, then the victim's JWT out one bit at a time.

One OS process. Two tenants. A boundary made of JavaScript.operating-system processattacker isolatefetch("https://victim.example")summons the victim into this processordinary JavaScript, no native codevictim isolateits own heap, its own globalsno shared memory, no threadsJWT in its address spacethe clock, from outsideWebSocket to a timestamp serversub-millisecond resolutionthe platform’s own timers stay frozenshared CPU: branch predictor, L1 cache, tree-PLRU replacementthe isolate boundary does not exist down here — one cache event is amplified into a run long enough for a blurry clock to readRESULT: the victim’s JWT, one bit at a time — up to 12 bit/s at 99.16% accuracy, 360× the 2021 rate
The attacker never leaves its own isolate. It calls the victim so the scheduler places it in the same process, borrows a clock from a WebSocket outside the platform, amplifies one cache event into a measurable run, and reads the neighbour's token bit by bit through hardware both isolates share.

The fixes say more than the finding

Cloudflare shipped three mitigations. Read them together.

They integrated the V8 Sandbox, which removes raw 64-bit pointers from the JavaScript heap region and kills the typed-array gadget. They taught DyPrIs to treat long-lived Durable Objects and I/O-heavy workloads as first-class security cases rather than noise. And they deployed Memory Protection Keys, putting each tenant's heap behind a hardware-enforced access boundary inside the shared process.

All three reach below JavaScript for something firmer than JavaScript semantics, and the third one lands on the CPU itself. Cloudflare is candid that the V8 Sandbox "is not a complete Spectre mitigation." Engineers who fix a language-level boundary by adding a hardware-level boundary have told you where they think the real line sits.

A coding-agent blog, writing about a CDN

The isolate has left the CDN. In March, Cloudflare shipped Dynamic Workers under the title Sandboxing AI agents, 100x faster, a way to spin up a Worker at runtime with code that did not exist a second ago. If you have run an agent that writes and runs its own scripts, you know the motivation: "if an agent (or an MCP server) is going to execute code generated on-the-fly by AI to perform tasks, that code needs to run somewhere," and "you can't just eval() AI-generated code directly in your app."

Cloudflare names the mechanism in the same post: "Dynamic Workers use the same underlying sandboxing mechanism that the entire Cloudflare Workers platform has been built on since its launch, eight years ago: isolates." Speed is why. An isolate boots in a few milliseconds on a few megabytes, roughly 100 times faster and 10 to 100 times more memory-efficient than a container, so a fresh sandbox per request becomes affordable instead of a warm pool you reuse and weaken with each turn.

Cloudflare is open about the cost. Their own beta post says it in eight words: "V8 security bugs are more common than hypervisor vulnerabilities." They wrote that months before anyone leaked a token.

So the story here is a trade, written down by the people making it, and now priced by researchers at 12 bits a second. The question lands on everyone else. When the code being sandboxed was written by a model, shaped by whatever a package or a web page told it, and left running next to a bearer token, is that the trade you want?

Bromure draws the line at the hypervisor

Bromure Agentic Coding does not run your agent in a language-level container next to other people's code. Each workspace is a Linux virtual machine on Apple's Virtualization.framework, Apple Silicon only: its own kernel, its own guest memory, its own disk cloned from a read-only base image. No shared process means no co-tenant to summon, and the boundary your agent runs behind is the hypervisor, which is the side of Cloudflare's own comparison with the rarer bugs.

That is the architecture. What would have made this particular attack pointless is narrower, and it sits one step further in.

The token was never in the box

What the researchers walked away with was a JWT sitting in the victim's address space, which is where a running service keeps the credential it uses. They got no shell and no root.

A Bromure workspace does not keep the credential there.

Every secret in a profile stays on your Mac. That covers the ANTHROPIC_API_KEY, the GitHub personal access token, the AWS access key, the kubeconfig bearer token, the container-registry Basic-auth blob and the Linear key. What Bromure injects into the VM is a fake: a brm_… placeholder, a throwaway client certificate, a derived Basic string. The agent reads it, the SDK reads it, git and kubectl and doctl read it, and the work goes through, because the MITM proxy on the host substitutes the real value on the outbound request. AWS gets an extra turn of the screw. The host re-signs the SigV4 request with the real material, so anything that routes around the proxy gets InvalidSignatureException back from Amazon instead of a working call.

So an attacker holding a perfect arbitrary read inside that VM, the primitive the researchers spent a paper building, reads a decoy. The bytes are there, they parse, they look like a key, and they buy nothing off the machine. You get that whether or not the isolation boundary is flawless, because the workspace is empty of the thing worth stealing.

CONVENTIONAL: the boundary is software, and the secret is inside itagent code in an isolateshares one OS process with other tenantsREAL TOKEN in its address spaceshared hardware underneathcache and branch predictorcarry the bits the boundary blockedthe neighbourreads the token12 bit/s, 99.16%BROMURE: the boundary is the hypervisor, and the secret is on the other side of itthe workspace VMown kernel, own guest memory, no co-tenantsANTHROPIC_API_KEY=brm_…a fake — an arbitrary read gets a decoyMITM proxy, host-sideswaps the fake for the real valueegress rules decide where bytes may gooptional consent dialog per usethe real APIsees the realcredential, sentfrom your Macevery swap, every allowed and denied connection: a row in Window → Security Timeline, written outside the VM
Conventionally the sandbox boundary is the only thing between a neighbour and your credential, and the credential sits inside the sandbox. In a Bromure workspace the boundary is the hypervisor and the credential sits on the other side of it: the guest holds a fake, and a proxy the guest cannot reach substitutes the real value on the wire.

Twelve bits a second still needs somewhere to go

A side channel is half of an exfiltration. The other half is a route out, and 12 bits per second is a route that has to stay open for a while. A 400-character token means minutes of traffic to a destination the attacker controls.

A Bromure profile carries its own egress rules, written as a short pf-style policy and edited as a table. Your Mac enforces them, in the virtual switch and the proxy, and nothing inside the guest gets a say: a denied connection gets a reset, and the decision lands as an egress.firewall row in Window → Security Timeline with the host, IP, port and verdict. Code running in the workspace sits on the wrong side of the rules it would need to change.

You can also put a hand on each spend of a real credential. Switch on Require approval to use per secret: the model API keys, SSH signing, GitHub and GitLab and Bitbucket tokens, each kubeconfig context, DigitalOcean, every AWS SigV4 signature, each container registry, and any manual token rule you add. The swap then pauses for a dialog on macOS, and you grant it for five minutes, an hour, or the session. The guest never gets to answer that question.

The trade is the story

Isolates are a good piece of engineering, and Cloudflare has handled this the way you would want: coordinated disclosure, three real mitigations, no hand-waving about what the V8 Sandbox does and does not cover. Millions of requests a second do not run on containers for a reason.

Cold starts are what won the argument on a CDN, and your coding agent is not answering an HTTP request in four milliseconds. It runs for an hour, installs packages nobody has audited, executes test suites a model wrote, and reads web pages that would like a word with it. Whatever runs that workload holds your tokens for the duration, and the one thing you know about the code it runs is that you did not write it.

For that job, a few hundred milliseconds of VM boot is not a cost worth optimising away. Cloudflare already published the exchange rate: V8 bugs are more common than hypervisor bugs. Bromure takes the other side of that trade, then takes the token out of the room as well.


Sources: Schwarzl, Xiao, Pedersen, Ainsworth, Topham, "Remote-Timer-as-a-Service: Efficient Microarchitectural Leakage in the Cloud with Remote Timers" (arXiv:2608.17043, Aug 17, 2026) · Cloudflare, "A revisit of remote Spectre attacks on Cloudflare Workers" (Aug 19, 2026) · Cloudflare, "Sandboxing AI agents, 100x faster" (Mar 24, 2026) · The Hacker News, "Cloudflare Workers Spectre Attack Leaks JWT From Co-Located Worker at 12 Bits/Second" (Aug 19, 2026)