Back to all posts
Published on · by Renaud Deraison

The block was the delivery

At DEF CON 34 this week, Tenet Security showed an attack that starts with a request your firewall is certain to refuse. The 403 is the point: the WAF writes the blocked request into the security log, verbatim, and days later a coding agent reads that log to debug the 403s and follows the instructions inside it. No step in the chain is unauthorized. Bromure Agentic Coding scores the text on arrival and keeps the credential the hijacked agent needs out of the machine it runs on.

The firewall worked. It matched the rule, returned a 403, and wrote the request it refused into the log, as designed. That log entry is how the payload reached the coding agent.

DEF CON 34 closed this morning in Las Vegas. On Friday, Barak Sternberg, Nevo Poran and Ron Bobrov of Tenet Security gave a talk whose title is also its argument: Your WAF Blocked Us, That Was The Exploit. The schedule abstract reads "Remote Agent Takeover via Cloudflare, Sentry and Claude Zero-Day for data exfil." The synthesis coverage landed on August 8.

The same team named Agentjacking in June, after showing that a fake Sentry error report retrieved over MCP would get a coding agent to run attacker-controlled code in 85% of attempts across more than a hundred live instances. Friday's talk extends that into Sentry's own Seer agent, whose poisoned recommendations then flow downstream into Cursor. The Cloudflare chain is the new material.

The 403 is the point

Start from the attacker's problem. They cannot reach your coding agent. It runs on a developer's machine behind everything you own, and they hold no credential, no session, and no route in.

They send a request built to be refused.

The WAF matches a managed rule and returns 403. It also does the other half of its job and records the request it blocked, payload and all, into the security event log. A log entry that omitted the payload would be useless to the analyst it exists for.

Days later, someone notices the endpoint throwing 403s and asks their agent to look into it. The agent calls the Cloudflare observability MCP server, one of the official servers Cloudflare publishes for this, and pulls the blocked-request rows. The attacker's text arrives inside a tool_result block, from a first-party vendor service the developer connected, under a field that means this is a request we rejected.

The agent reads it as instructions. Then it uses its own authorized Cloudflare tools to rewrite DNS records and reroute customer traffic. Tenet puts the exposure at more than 15,000 organizations, including 27% of the Fortune 1000.

Every step below is doing its job1. a requestbuilt to be refused, aimedat a public endpointno access needed2. the WAF blocks itmanaged rule matches: 403and records the requestverbatim, as it should3. someone asks"why is checkout throwing403s? have a look"an ordinary Monday task4. the agent readsobservability MCPserver, first-party,wired in on purposetool_result: blocked_request.bodythe attacker's text, unmodified, in a field whose name means "this is the request we stopped"the agent rewrites DNS with its own toolscustomer traffic reroutes; every call is authorized
The block is what delivers the payload. The attacker never reaches the agent, so they send a request the WAF is certain to refuse, and the WAF files the payload verbatim in the log. The agent fetches it later over MCP, from a vendor service, wrapped in a field name that means the request was stopped.

Nothing unauthorized happens

Tenet calls the pattern the Authorized Intent Chain, and the June write-up states the consequence:

Walk the chain again and try to find the step to alert on. The developer authorized the agent. The agent was configured with the MCP server. The MCP server queried logs it is meant to query. The account it used holds the DNS permission you granted it on purpose. And the request that started all of it was blocked. Every control in the path did the thing it was bought for, and the composition of those correct behaviors is the attack.

Anything that works by asking who is doing this and are they allowed to will answer yes at every hop, because the answer is yes. Two moves are left. Read the text on its way in, and put the enforcement somewhere the hijacked agent cannot reach.

Score the text on arrival

Bromure Agentic Coding runs the agent inside a disposable Linux VM on Apple Silicon and routes every byte of its traffic through a proxy on the host, outside the box the agent runs in. That includes the model traffic, which is where log rows turn into instructions.

Tool output arrives untrusted

Bromure scores the content the agent reads, including file contents, web pages, and the tool_result blocks that come back from MCP servers and shell commands, with a local prompt-injection model. That detector looks for this: a log row carrying an imperative aimed at whoever reads it. It scores the span itself, so Cloudflare delivering it over a connection you configured buys the payload nothing.

You choose what a detection does

Log and continue records each hit to the Security Log window and lets the request proceed. Ask me what to do pauses the outbound request and shows you the flagged text, so you decide with the sentence in front of you. Block unilaterally stops it before the model reads it, and the agent gets a hard 451 it reports as an API failure.

Authority files get their own classifier

Config poisoning was the other half of the DEF CON disclosure. A fine-tuned ModernBERT classifier scores the files an agent treats as standing orders, CLAUDE.md and AGENTS.md and GROK.md and their nested and global variants, alongside a deterministic scanner for the zero-width, bidirectional and Unicode-tag payloads no model can read.

It stays on your Mac

Both detectors run on-device, from models you download once from bromure.io. Scoring your production WAF logs for injected instructions does not involve shipping your production WAF logs anywhere.

The write needs a key that is not in the machine

Suppose a payload gets through anyway. The end of this chain is an API call that changes a DNS record, and that call needs a credential.

In a Bromure profile, it is not there to take. A Cloudflare token goes in as an Other API keys entry, which mints a fake brm_… value and exports it into the VM under the env var name you choose. The real token stays on the host, and the proxy substitutes it on the wire, on outbound requests to the one destination host you bound it to. The agent's process never holds it. Neither does a dotfile or a shell history.

Turn on Require approval to use for that entry and every substitution raises a dialog on macOS, outside the VM, before Bromure forwards the real value. A hijacked agent can compose a well-formed DNS update, and it cannot answer the dialog.

The rest of the blast radius has a stricter answer. Guardrails is a policy engine that runs in the host proxy and strips destructive operations out of the protocols the agent speaks: Kubernetes, AWS, DigitalOcean, container registries, GitHub, GitLab, Bitbucket, and the HTTPS database endpoints. Choose Block destructive or Read-only per resource, and a blocked call comes back as a hard 403. Because the host makes that decision, an agent following someone else's instructions inside the VM has nothing to switch off.

On the developer's laptopthe log row arrivesfirst-party MCP server, so it is trustednothing reads the text itselfthe token is right thereCLOUDFLARE_API_TOKEN, live, usablethe DNS write goes out and succeedsand no alert firesauthorized user, authorized agent,authorized token, authorized callInside a Bromure profilethe row is scored on arrivala local model reads the span, not the senderask, or block with a hard 451the token is not in the VMenv holds brm_…, real value on the hostthe swap raises a consent dialog on macOSand the chain is a list of rowshost, status, latency, swap reportper request, in the Trace Inspector
The same hijacked agent, on two machines. On a developer's laptop the token is in the environment and the call goes out. In a Bromure profile the log row is scored on arrival, the environment holds a placeholder, the real token is substituted on the host behind a consent dialog, and the whole exchange is a row in the Security Log.

A sandbox bug does not move a hypervisor

The talk also disclosed a Claude zero-day that bypasses the agent's network sandbox for full data exfiltration, plus agentic rootkits assembled from memory injection and config poisoning that no EDR picks up. Both findings point at one weak spot: the containment an agent ships with is reachable from inside.

Bromure puts the boundary somewhere else. The boundary is a virtual machine, running on Apple's hypervisor with its own kernel, on NAT, with every packet crossing a proxy that runs on macOS. A defect in the agent's in-process sandbox is a defect inside the VM. It does not relocate the VM, and it does not give code in the VM a route to the network that skips the host proxy.

When something does land there, it lands on a machine built to be thrown away. Erase home wipes /home/ubuntu, meaning dotfiles, .ssh, npm-global, .cargo and shell history, and resets it to its post-clone state. Reset to base discards every system-level change and re-clones the workspace disk from the base image. The rootkit no scanner can find still sits in a disk layer you can delete.

The invisible chain, written down

Tenet's strongest observation is that the chain hides in plain sight because each step in it is legitimate. There is no signature and no anomaly to correlate.

Bromure does not try to correlate one. Every request the VM makes crosses the host proxy, and Bromure records the host, the status, the latency, and which credentials it swapped in. Injection detections land in the Security Log with the flagged text attached. Raise the trace level and Bromure keeps the bodies of the AI requests too, encrypted with the same keychain key as your profile secrets.

You get to ask a different question: what did this session touch. An agent that spent the afternoon reading Cloudflare logs and then made its first call ever to the DNS API is two lines you can read, on the machine the session ran on, with no agent to deploy and no SIEM to route it to.

Cloudflare will tune the surface, Sentry will tune theirs, and the two chains from Friday's talk will close. Pair any system that records hostile input with any agent that reads records to do its job, and you have rebuilt this attack out of two parts that both do their jobs. You keep logs so someone reads them, and the reader is now a program that acts on what it finds.

One instinct covers it: treat everything the agent reads as something a stranger may have written, including the parts your own defenses wrote down.

Install Bromure Agentic Coding, and put something that checks the text between the log and the agent.