Claude Code’s SOCKS5 Proxy Bypass: Why Egress Filtering Must Happen at the Boundary | by Hideaki Takahashi | Jul, 2026 | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
Claude Code’s SOCKS5 Proxy Bypass: Why Egress Filtering Must Happen at the Boundary
Hideaki Takahashi
5 min read·<br>Just now
Listen
Share
Press enter or click to view image in full size
A JavaScript endsWith() check and libc's getaddrinfo() disagreed about where a hostname ends. That parser differential defeated Claude Code's network sandbox, and it is a lesson about the layer at which you enforce egress, not just one bug.<br>What happened<br>Security researcher Aonan Guan publicly disclosed a complete bypass of Claude Code’s network sandbox. It was his second such bypass, and he was explicit that he saw it as a consistent implementation-pattern failure rather than an isolated bug.<br>Claude Code’s sandbox routes outbound traffic through a SOCKS5 proxy. The proxy is responsible for enforcing the user’s egress allowlist, for example, allowing connections to *.google.com while blocking everything else. It does this by validating the requested hostname in JavaScript, using an endsWith()-style string check against the allowed suffixes.<br>The bypass uses a hostname with an embedded null byte:<br>attacker-host.com\x00.google.comThe two components of the sandbox read this string differently:<br>The JavaScript filter sees the whole string, notices it ends with .google.com, and approves the connection.<br>libc’s getaddrinfo() treats the string as a C string and terminates at the null byte (\x00). It resolves only attacker-host.com, the blocked host.<br>So the allowlist check validates one destination while the actual DNS resolution and connection go to a different, blocked one. The parser differential smuggles a blocked destination straight past the allowlist.<br>The issue affected every Claude Code release from v2.0.24 (the sandbox’s general-availability release on 2025–10–20) through v2.1.89, roughly 130 versions over about 5.5 months. Anthropic silently patched it in v2.1.90 (2026–04–01), with no security note in the release notes. No CVE had been assigned as of May 2026.<br>Root cause<br>The network allowlist was enforced at the application layer: a SOCKS5 proxy plus a string comparison over hostnames. Any application-layer name match is only as trustworthy as the agreement between the code doing the matching and the code doing the resolving. Here they disagreed.<br>A JavaScript string and a C string do not have the same notion of “where the hostname ends.” JavaScript strings can contain null bytes as ordinary characters; C strings cannot. When one layer decides “allowed” from the full string and a lower layer acts on a truncated prefix, the decision and the action are about different destinations. The allowlist was never actually enforced on the thing that connected.<br>This is fragile by construction. Hostname parsing has a long history of differentials (case, trailing dots, IDN/punycode, embedded control characters) and the null-byte truncation is one of the sharpest. Enforcing egress by matching names in application code means you are betting that no such differential exists anywhere between your check and the kernel.<br>Impact<br>The sandbox’s network containment was defeated. An attacker who could get the agent to make an outbound request (for instance, via prompt injection in fetched content) could exfiltrate the entire contents of the sandbox (credentials, source code, private keys) to an arbitrary server, past an allowlist that was supposed to permit only a small set of trusted domains.<br>How h5i env changes the outcome<br>Press enter or click to view image in full size
The lesson here is where egress is enforced, and this is what we can solve with h5i, an OSS auditable workspace for AI coding agents. h5i env's supervised tier enforces net.egress at L3/L4 with the kernel, not by parsing hostnames in application code.<br>h5i env create jail --isolation supervised # net.egress allowlist, fails closed[profile.agent-claude<br>isolation = "supervised"<br>[profile.agent-claude.net]<br>mode = "deny" # or an egress allowlist:<br>egress = ["registry.npmjs.org:443", "github.com:443"]<br>[profile.agent-claude.env]<br>pass = ["PATH", "HOME", "LANG"] # no cloud creds inheritedIn supervised, the confined process runs in an always-on network namespace with uplink via slirp4netns. Egress is enforced by an nftables default-drop ruleset: packets to any destination not on the allowlist are dropped by the kernel packet filter. DNS is pinned via a private /etc/hosts and there is no port 53: there is no resolver in the box for a crafted hostname to feed. Because enforcement operates on the IP/port of the actual packet, there is no separate "name check" that can disagree with a later "name resolution." A null-byte truncation trick has nothing to bite on: the kernel is filtering packets, not comparing strings.<br>The isolation stack is also designed so confined code cannot open its...