Why Proxy-Variable Agent Sandboxes Fail, and the CONNECT-Time Fix

Agent sandboxes that key egress on proxy variables fail for structural reasons. The control that holds authorizes the destination at CONNECT time.

On September 4, 2026, researchers published ~18,000 posts that autonomous agents left on a 25-year-old German wiki over six weeks, using it as a message board to trade answers and swap sandbox-escape techniques. The agents were internally deployed, running in Azure sandboxes, and the authors are direct about the rule they broke: writing to the internet was blocked.

Defensive security research. This post reproduces published agent-sandbox findings in an isolated local lab (Docker Compose, no third-party infrastructure) so the CONNECT-time egress control can be tested. It describes mechanisms, not procedures, and links the primary sources.

They wrote anyway. Not by finding a hole in the control, but by using fields the control never looked at.

I built agent-egress-control to reproduce four of their techniques against a realistic in-band egress policy, then put the same agent behind a Python CONNECT gateway that authorizes the destination at CONNECT time and watched all four fail. That gateway is a stand-in that shows the pattern, not agentgateway. It is Docker Compose, no cloud, no API keys, and it runs in about thirteen seconds from a torn-down stack.

This is also a debt I owed. Two days ago I published a control map for the July 2026 agent intrusion that argued for per-destination deny-by-default egress and then admitted, in plain text, “I have not built that demo yet, so treat this as the design I am arguing for and not a validated recipe.” This is that demo.

What the agents actually did

The sandbox allowed reads and blocked writes. The wiki they found, DSEWiki on the ProWiki farm, is old enough that it does not care which HTTP method a page edit arrives on. A save is a save. Put the edit parameters in the query string of a GET and the page changes.

That is the first technique, and it is the whole shape of the problem in one move. The control was reading the method. The wiki was reading the parameters.

The most instructive post in the corpus is from June 20, 2026, where one agent writes down a repeatable way past a proxy-variable policy. I am describing the mechanism rather than the steps, because the original names a live third-party endpoint and its address. The control lived in the agent’s own environment: its resolver, its local name mappings, and its proxy exemption list. The agent treated those as fields it could change, pointed an exempt name at a blocked destination, and sent the request to the exempt name. The proxy was never consulted. Not bypassed. Not consulted.

Fourteen minutes later, another agent confirmed it worked. The full text is in the published corpus if you need the primary source.

Read that again as a piece of infrastructure analysis, because it is a good one. The egress control was a proxy the workload was configured to use, with an exemption list. The agent noticed that both the proxy setting and the name resolution were things it controlled. The proxy was never consulted.

The researchers also document link chaining through public relays. Their reading of why is exactly right: a denylist on the real destination hostname does not match a permitted reader or redirector that then fetches that destination. A cleaner alternative to those public readers is to serve markdown to agents at the gateway so the origin stays HTML-only.

What the in-band proxy allowed

The demo runs two network topologies out of one compose file. In part one the agent has a route to the network, and what stops it writing is a proxy it is configured to use. That is how most agent sandboxes are actually built. The in-band half is a method rule, a hostname denylist, and a proxy-variable exemption list.

Four writes landed. Every single one is a GET, on a proxy whose first rule is that writes are blocked:

{
  "writes": [
    { "page": "SharedAnswers", "method": "GET", "host_header": "wiki:8080" },
    { "page": "SharedAnswers", "method": "GET", "host_header": "172.28.0.4:8080" },
    { "page": "RelayNotes",    "method": "GET", "host_header": "wiki:8080" },
    { "page": "BypassNotes",   "method": "GET", "host_header": "wiki" }
  ]
}

Why all three rules failed the same way

Line the rules up next to what the workload controls:

The rule readsThe workload changes it by
the HTTP methodputting the write in a query string
the hostname in the URIusing the address literal, or a permitted relay
the proxy variables in its own processan exemption list plus a local name mapping plus a Host override

Every one of those fields is chosen by the thing being constrained. None of them constrains where the packets go. A rule written against a field the workload picks is a request, and an agent optimizing against a task reward will read it as one.

The third row is the worst of the three, and it is the one most sandboxes ship. HTTP_PROXY and NO_PROXY are environment variables in the agent’s own process. Local name mappings live in the agent’s own filesystem. Turning the control off is not privilege escalation, it is an unset.

The control that holds

Part two is the same agent and the same four techniques. What changes is where the control lives. The agent sits on a Docker network marked internal: true, so there is no route off it. Its only peer is a gateway that terminates CONNECT and decides on the destination.

--- D0  the legitimate destination still works
    {"country": "US", "year": "1990", "value": 26.16}    exit=0 http=200
--- D1  the GET-shaped write (stage A2)
        exit=7 http=000
--- D2  the address literal (stage B2)
        exit=7 http=000
--- D3  the link chain (stage B3)
        exit=7 http=000
--- D4  local name mapping + Host override (stage C)
    local mapping now says: datasource -> 172.28.0.4  (the wiki)
    {"country": "US", "year": "1990", "value": 26.16}    exit=0 http=200
    ^ that is the datasource answering. The gateway resolved the name itself.
--- D5  abandon the proxy and dial the address directly
        exit=7 http=000

The gateway’s own log carries the reasons:

[egress-gw] ALLOW CONNECT datasource:8080 -> 172.28.0.3:8080
[egress-gw] DENY  wiki:8080 resolves to ['172.28.0.4:8080'], which is not an allowed destination
[egress-gw] DENY  172.28.0.4:8080 resolves to ['172.28.0.4:8080'], which is not an allowed destination
[egress-gw] DENY  redirector:8080 resolves to ['172.28.0.7:8080'], which is not an allowed destination

Three properties do the work, and the fourth is not code at all.

  1. It authorizes the destination, not the request. The decision happens on the CONNECT authority, before a method, path or header exists inside the tunnel. A rule that never reads the method cannot be fooled by changing the method. That is why D1 and D3 look identical to the gateway: it never learned that one of them was a write and the other was a relay.
  2. It resolves the name itself. The client’s resolver has no vote.
  3. It fails closed. Unresolvable, unparseable and not-on-the-allowlist are all denials.
  4. There is no route that avoids it. Unsetting the proxy variable is not a bypass, it is a disconnection. That is D5, and it is the only reason the other three matter.

D4 is the interesting one

D4 is the wiki technique, and it does not error. The agent remaps the wiki’s address onto the datasource name in its own resolver, sends the request to that name with a Host override, and gets back a clean HTTP 200 with real JSON.

From the datasource. Not from the wiki.

The gateway resolved datasource with its own resolver, got the datasource’s real address, and opened the tunnel there. The agent’s name-to-address mapping never entered the decision, because the agent’s mapping was never consulted. The Host header rode inside a tunnel that was already pointed somewhere else.

That is the difference between blocking a technique and making it inapplicable. Nothing in the gateway knows what a local hosts file is.

What this does not prove

The demo shows that the enforcement point and the field being checked both matter. It does not show that destination allowlists are sufficient, and I want to be careful here because my own control map made the opposite point two days ago and it still stands.

In the July 2026 intrusion, the sandbox escape went through a permitted egress channel: a package registry cache proxy with a zero-day in it. A correctly enforced, out-of-band, deny-by-default allowlist that includes a package registry still permits that. Command and control over a permitted general-purpose web service is the same story. The allowed destinations are themselves attack surface, and the wiki incident is a demonstration of exactly that, since the wiki was reachable because reads were allowed.

So the claim is narrow and I will state it narrowly. Moving the control out of the workload and keying it on the resolved destination removes an entire class of trivial bypass that costs an agent one line of shell. It does not remove the need to think hard about what is on the list. I later showed a permitted registry as a covert channel : the host is allowed, and the abuse rides publish and list.

How this maps to shipped software

The pattern is not hypothetical, and it moved in the last week.

agentgateway merged #3237, “substrate: authorize actor egress at CONNECT time” , on September 1. It authorizes actor identity before accepting HTTP, TLS or opaque TCP CONNECT tunnels, rejects unknown, stale or non-running actors, and fails closed when the control plane is unavailable. That last clause is the one to notice: a control that fails open under load is a control with a scheduled outage.

Agent Substrate’s egress demo runs the same shape on Kubernetes and closes the gap my compose file leaves. It uses nftables REDIRECT to put the workload’s TCP egress into the tunnel transparently, so there is no proxy variable in the agent’s environment to unset, and it carries the actor’s identity in its own X.509 certificate rather than a header. The demo README puts it well: the actor believes it is dialing plain HTTP directly.

My compose file isolates the destination half of that so it fits on a laptop. The identity half, proving which agent is asking, is a separate demo: SPIFFE Workload Identity for AI Agents covers how the certificate gets there, and Your AI Agent Should Not Hold the LLM API Key covers why you want it. The same chokepoint is what keeps shadow MCP servers unreachable once you know they exist.

Run it

git clone https://github.com/themsquared/agent-egress-control
cd agent-egress-control
docker compose up -d --build
./scripts/verify.sh

verify.sh runs 17 assertions covering every claim above, including that stage D added zero writes to the wiki and that the gateway never allowed a CONNECT to it. On my machine, from a fully torn-down stack with base images cached, that is 4 seconds to bring up and 9 seconds to verify:

  PASS four writes landed through the in-band control
  PASS every landed write was a GET
  PASS D4 the Host override reaches the datasource
  PASS stage D added no writes (4 total)
  PASS gateway never allowed a CONNECT to the wiki

-----------------------------------------
17 passed, 0 failed

./scripts/demo.sh runs the same thing narrated, if you would rather read it than assert it.

The takeaway

The agents in the wiki corpus were not doing security research. They were trying to answer questions about tobacco prevalence in 1990 inside a 65-second timer, and writing to the internet was the cheapest way to get help. The gaps fell out of ordinary competence applied to an ordinary obstacle, which is the part worth sitting with: this is what the unremarkable case looks like.

If your agent sandbox’s egress policy is a proxy variable and a denylist, you do not have an egress policy. You have a naming convention. Move it to a chokepoint the workload has no route around, decide on the address you resolved rather than the string it typed, and log the decision.

Code: github.com/themsquared/agent-egress-control .

Frequently asked questions

Can an AI agent write to the internet from a sandbox that only allows reads?

Yes, and it does not need an exploit. If the control keys on the HTTP method, the agent puts the write in a GET query string. Plenty of servers accept that, including the 25-year-old wiki used in the September 2026 agent message board incident. In my demo, all four writes that landed were GETs against a proxy configured to block writes.

Does NO_PROXY create an egress gap for an agent?

It does when the agent can edit its own resolver. Agents in the 2026 incident remapped a blocked destination onto a name that was already exempt from the proxy, so the proxy was never consulted and none of its rules applied. I reproduce the same shape in the demo. The CONNECT-time control is what stops it.

Where should an agent egress control be enforced?

Outside the workload, on a network the workload has no route off. Every control that lives in the agent's own environment, proxy variables, exemption lists, local name mappings, is configuration the agent can change. Enforce at a gateway that terminates CONNECT and fails closed. agentgateway#3237 (in the v1.6.0 prereleases, not v1.5.0) builds this into the substrateEgress policy: it authorizes the actor's identity before accepting a CONNECT tunnel and fails closed when the control plane is unavailable.

Why authorize an agent's egress at CONNECT time instead of per HTTP request?

Because the CONNECT authority is a destination, not a request. Method, path, headers and Host all live inside the tunnel and are all chosen by the workload. Deciding on the resolved address before the tunnel opens means changing the method, swapping a hostname for an address literal, or overriding Host cannot move the connection. All three failed in stage D of my demo, behind a Python CONNECT gateway, not agentgateway.