โ† Writing 4 min read AI image

Credential proxies fail open in the details

This autumn's advisories for agent sandboxes say the same thing: the allowlist was rarely the problem. The leaks came from recognition, from parsing and from error paths.

.md

A credential proxy keeps an agent's real token on the host: it puts the token into each request as it leaves the sandbox, and masks any real token in what comes back. Many of them can be set to allow only a fixed list of destinations. Their bugs this autumn were elsewhere:

  • Docker Sandboxes, CVE-2026-105570: the check that decided whether to mask a token response compared hostnames case-sensitively, routing did not. A case variant of the endpoint returned real access and refresh tokens to the sandbox.
  • Docker Sandboxes, CVE-2026-101998, in its own words: "could fail open while masking credentials in protected proxy responses". A body read returned data and an error together, and the unmasked bytes went through.
  • microsandbox, 28 September: a hostname allowlist bypass "via mismatched HTTP Host/:authority", affecting "especially deny-by-default configurations" (GHSA-3mpp-52gc-935r). And on 9 October: "Host/SNI check fails open on malformed HTTP/1 request lines" (GHSA-gj24-29j2-3x78).

None of these came from the allowlist: no entry was missing, and none was wrong. Each is a place where the proxy met something it did not expect and let it pass.

Three decisions, one default

A proxy decides three things, and each needs the same answer for the unknown: refuse.

What may go out. Listed hosts, methods and exact paths; everything else is a named refusal in the log. This is the part everyone has.

What a request is. The CONNECT target, the TLS server name, the Host header and whatever the proxy routes on must all be one name, read one way. Every normaliser is a second parser, and three of the four bugs above are one name read two ways. Either canonicalise once and use only the result, or refuse anything not already canonical. Anthropic's new srt-proxy pins it: "the Host header and the TLS server name must name the CONNECT target".

What comes back. Masking is code, and code has error paths. A read error, an unknown flow, an unparseable body: if the default there is forward, the proxy fails open exactly where it matters most.

What d0 does

d0 is a private tool of mine, a productivity tool for my own agentic coding, not a product: it starts Claude Code, Codex or Antigravity in a throwaway Firecracker microVM, hands them a task and brings their work back as a branch. Its proxy takes the strict end of each decision:

  • One host per CLI, a few exact methods and paths, nothing else.
  • The TLS server name and the Host header must equal that host byte for byte. No lowercasing, no trimmed dot: an odd spelling is refused, not repaired.
  • The refresh token never leaves the host. No token endpoint is on the list, so there is no token response to mask. The safest masking code is the code you do not need.
  • A response whose status line or headers carry the real token is not passed on.
if r.Host != host && r.Host != host+":443" || r.URL.IsAbs() || r.URL.RawPath != "" ||
	r.URL.EscapedPath() != r.URL.Path || !maskedPathAllowed(credentials, r.Method, r.URL.Path) {
	status, reason = http.StatusForbidden, "path-refused"
}

One of the three CLIs d0 runs, Antigravity, keeps its token inside the VM. Google lets its unmodified CLI be run by one's own tools as long as no credential is "extracted, read, or passed to the external tool" (Google staff, October 2026), and masking would have to read it. So d0 only passes Antigravity's traffic through, and every such run says so. Whether that setup is fine, I asked in Google's forum; an answer is still open.

The price

Strict costs something, and it shows. On 3 November I wanted OpenAI's Daybreak, a per-request switch for security work, in a d0 run. Codex refused before sending anything: "Daybreak requires the OpenAI model provider". d0 routes Codex through its own provider, so that the proxy sees every request. A looser proxy might have let the feature work. This one made the conflict visible on the first try, which is the point.

That only works at my scale: one user, a few CLIs, Linux. A product for every agent and every company would drown in refusals.

Four questions for any credential proxy

  1. What happens to a request it does not recognise?
  2. Do SNI, Host and the routed name have to be the same string?
  3. What does the masking code do on a read error?
  4. Does the sandbox ever talk to a token endpoint at all?

Written with the help of AI; the thinking and the final say are mine.