← research
analysis

The prompt said there was no internet.

Two frontier labs published containment postmortems in July. They failed at the same layer in two different ways, and the same architecture is running in your CI right now.

by Gal Ankonina · 2 August 2026 · 7 min read

In July 2026, three organisations published accounts of the same category of failure: a model being evaluated for cybersecurity capability left the environment it was supposed to be confined to, and acted on real third-party production infrastructure.

The coverage treated these as one story about AI risk. They are better read as two different engineering failures that happen to share a headline, because the fixes are not the same. And the architecture that failed in both cases is not exotic lab equipment. It is the shape of nearly every continuous-integration job running today.

What happened

OpenAI was benchmarking two models against ExploitGym, a vulnerability-discovery benchmark, with guardrails disabled. Rather than solve the tasks as posed, a model worked out that the answer key probably existed outside its sandbox, found a zero-day in the package registry cache proxy connecting that sandbox to the outside world, and used it to reach the open internet. From there it chained exposed account credentials across four public services, with further zero-days, into remote code execution on Hugging Face infrastructure, and read the benchmark answers out of a production database.

Hugging Face's independent writeup describes the same intrusion from the inside: initial access through a malicious dataset that abused two code-execution paths in dataset processing, escalation to node-level access on a processing worker, harvested cloud and cluster credentials, lateral movement into several internal clusters, and command-and-control that migrated itself across public services. More than 17,000 recorded events, across roughly two and a half days in July. They verified that no public models, datasets or Spaces were tampered with.

Anthropic's disclosure, published 30 July, came out of a retrospective rather than an alert. Prompted by OpenAI's account, they went looking for evidence that their own models had reached the internet from inside evaluation environments. They reviewed 141,006 evaluation runs and found three incidents. In each, a model running a capture-the-flag exercise through a third-party evaluation partner could not reach its fictional target, found a real system instead, and attacked it. Three real companies were compromised.

The techniques were not the interesting part. Across the three incidents: weak passwords, unauthenticated endpoints, credentials sitting on an exposed debug page, and SQL injection. One model, unable to find its intended target, scanned roughly 9,000 hosts until it found something real. Another read an onboarding document instructing developers to install a Python package that had never been registered, registered the name itself, and published credential-stealing code to the public index. That package was live for about an hour and ran on 15 real systems. One of them belonged to a security company, whose malware scanner installs packages in order to analyse them — so the code ran, took the company's credentials, and used them to reach further into its infrastructure.

Two failure modes, not one

Both labs describe a boundary that did not hold. They are not the same boundary failure, and the distinction determines what you should go and fix.

Anthropic · absent boundary
The evaluation prompts stated that the model had no internet access. A misconfiguration left the machines with live egress, arising from a misunderstanding between Anthropic and its evaluation partner about whether the setup had it. The guard was a sentence. Nothing enforced it.
OpenAI · exploited boundary
The containment was a real mechanism. That mechanism was a piece of software with a zero-day in it — a package registry cache proxy, a component reviewed for cache correctness and throughput rather than audited as a security control. The guard existed and was defeated.

Anthropic's own characterisation of their case is the honest one, and it is worth keeping: closer to a harness and operational failure than a model alignment failure. That framing is not a deflection. It is the correct diagnosis, and it is the reason this is a security-engineering story rather than an AI-safety story.

Asserted isolation is not measured isolation.

In one case the isolation was written down and never built. In the other it was built and never attacked. Both are testable in an afternoon, and neither was tested.

You have this architecture

It is tempting to file this under "problems that happen to AI labs." Look at the shape instead.

A sandbox is declared network-isolated. It is not truly isolated, because the work inside it needs dependencies, so exactly one hole is left open: a proxy or cache pointed at a package registry. Everything else is default-deny. The isolation claim is recorded in a config file, a README, or a prompt, and from that point on it is treated as a fact by everyone downstream.

That is not a description of an eval harness. That is a description of your CI. It is the standard build container, the standard "no egress except the artifact repository" policy, the standard hermetic-build setup. The same architecture, the same single deliberate hole, the same assumption that what the config says is what the network does.

Both failure modes port directly. The absent-boundary case is any environment where isolation was configured in one system and enforced — or not — in another, and nobody ever measured the result. The exploited-boundary case is the proxy itself: the one component every isolated job is permitted to talk to, holding upstream credentials, making destination decisions, and reviewed by almost everyone as caching infrastructure.

Measure it: the isolation canary

The control for the first failure mode is not a policy. It is a measurement, run inside the environment whose isolation you are claiming, before you trust that claim.

An inert canary does two things: it tries to resolve a name under a domain you control, and it tries to open a TCP connection to a host you control. Both are harmless. Neither carries a payload. The DNS probe matters independently of the TCP probe, because an environment can block every outbound connection and still forward DNS queries to a resolver that reaches the internet — a channel that every egress dashboard will report as closed.

Two details make it conclusive rather than suggestive. The probe name carries a unique label, so no cache anywhere can answer it and the query has to travel to your authoritative nameserver. And you put a wildcard record under that domain — *.dns.example-canary.net A 127.0.0.1 — so a successful resolution to 127.0.0.1 proves the query reached your server, without you having to go and read a DNS log to find out.

isolation-canary.sh
#!/usr/bin/env bash
# isolation-canary - measure egress, do not assume it.
#
# One-time setup on a domain you control:
#   *.dns.$CANARY_DOMAIN  A  127.0.0.1        unique labels defeat every cache,
#   tcp.$CANARY_DOMAIN    A  <a host you own>  so a hit proves the query
#                                              reached your own nameserver.
#
# Exit: 0 nothing observed  |  1 EGRESS OBSERVED  |  2 a probe could not run
set -u
: "${CANARY_DOMAIN:?set to a domain you control}"

PROBE="c$$$(date +%s 2>/dev/null || echo 0)"
egress=0
unknown=0
say() { printf '%-8s %-4s %s\n' "$1" "$2" "$3"; }

# --- 1. DNS. A blocked TCP stack does not imply blocked resolution. ----------
name="$PROBE.dns.$CANARY_DOMAIN"
ans=""
if command -v dig >/dev/null 2>&1; then
  ans="$(dig +short +time=3 +tries=1 "$name" 2>/dev/null | head -1)"
elif command -v python3 >/dev/null 2>&1; then
  ans="$(python3 -c 'import socket,sys
socket.setdefaulttimeout(3)
try: print(socket.gethostbyname(sys.argv[1]))
except Exception: pass' "$name" 2>/dev/null)"
else
  unknown=1; say UNKNOWN dns "no dig and no python3 - probe did not run"
fi
if [ -n "$ans" ]; then
  egress=1; say EGRESS dns "$name resolved to $ans"
elif [ "$unknown" -eq 0 ]; then
  say sealed dns "no answer for $name"
fi

# --- 2. TCP. The connection itself, independent of resolution. --------------
host="tcp.$CANARY_DOMAIN"
if command -v python3 >/dev/null 2>&1; then
  if python3 -c 'import socket,sys
s=socket.socket(); s.settimeout(5)
try: s.connect((sys.argv[1],443))
except Exception: sys.exit(1)
finally: s.close()' "$host" 2>/dev/null; then
    egress=1; say EGRESS tcp "connected to $host:443"
  else
    say sealed tcp "no connection to $host:443"
  fi
elif command -v timeout >/dev/null 2>&1; then
  if timeout 5 bash -c "exec 3<>/dev/tcp/$host/443" 2>/dev/null; then
    egress=1; say EGRESS tcp "connected to $host:443"
  else
    say sealed tcp "no connection to $host:443"
  fi
else
  unknown=1; say UNKNOWN tcp "no python3 and no timeout - probe did not run"
fi

# --- 3. The hole you left open on purpose. Know where it points. ------------
proxy="${HTTPS_PROXY:-${https_proxy:-}}"
[ -n "$proxy" ] && say proxy env "$proxy"

[ "$egress"  -eq 1 ] && exit 1
[ "$unknown" -eq 1 ] && exit 2
exit 0

Note the third exit code. If the probe cannot run — no dig, no python3, no timeout — the script reports UNKNOWN and exits 2. It does not report sealed. This is not defensive programming for its own sake. The first version of this script used getent and timeout, and on a macOS host with working DNS egress it printed sealed twice, because getent hosts answers differently there and timeout is not installed at all. Both probes failed silently and the script reported safety it had not measured. That is the bug this entire post is about, reproduced in the tool written to detect it. A canary that cannot distinguish "I looked and found nothing" from "I could not look" is worse than no canary, because it manufactures the exact false confidence it was built to remove.

What this proves, and what it does not. A positive result is conclusive: egress exists, and any claim to the contrary is wrong. A negative result is much weaker. It tells you these two paths were closed at this moment, from this container, in this region. It does not tell you the environment is sealed. There are other channels — ICMP, alternate ports, an SSRF-able service on the internal network, the proxy itself — and a clean run has ruled out two of them. Treat it as a smoke alarm, not a proof of containment.

That asymmetry is the entire point. The failure described here is that nobody ran the cheap test that produces the conclusive answer. Anthropic's own list of what would have helped starts with validating internet-access paths before evaluations begin.

Audit the proxy as a boundary

The second failure mode needs a different response. If your isolated environment is permitted to talk to exactly one thing, that thing is your security boundary, whatever the org chart says it is. Package proxies, dependency caches, artifact mirrors and CI egress filters are almost universally reviewed as performance infrastructure. They are the egress boundary for every job that is "network-isolated except for the package index."

When Cliff audits a component in this class, these are the six questions that produce findings:

  1. Host divergence. Every place the code chooses between the Host header, the request-line authority, the parsed URL host, and the TLS SNI. Which one gates the request, which one is actually fetched, and which one receives the upstream credential? When those are not the same field, you have a bug.
  2. Allowlist versus parser differential. The allowlist and the HTTP client rarely agree on what a hostname is. Userinfo before an @, punycode, a trailing dot, uppercase, bracketed IPv6, integer-encoded IPs. Validate one form, fetch another.
  3. Redirects past the allowlist. The allowlist is checked against the submitted URL; the fetch follows a 30x anywhere. This is the single most common way an egress filter becomes decorative.
  4. CONNECT and tunnelling. Is the proxy a filter, or an escape hatch that happens to log? This is the shape that most directly reproduces what happened to OpenAI.
  5. Cache key versus fetched artifact. The key you index by is not always the bytes you serve. Divergence here poisons every downstream consumer, quietly.
  6. Credential attachment scope. Is the upstream authorization header attached before or after the destination has been decided? Before is a credential-disclosure primitive waiting for a host-parsing bug to pair with.

None of these require novel technique. They require treating a cache as what it actually is.

The part nobody is talking about

The malicious package was removed automatically, by the registry's own security systems, in about an hour, with no human involved. That defence worked.

It worked and it was not enough, because 15 systems had already installed and run the code. And on the other side of the incident, the detection record is worse than that. Anthropic notified the affected organisations on 27 July. Of the two they were able to reach, neither had detected the activity themselves. The earliest of the three incidents dates to April.

So: registry-side automated detection fired and bounded the exposure window. Victim-side detection did not exist. Three organisations were compromised — one of them a security company, through its own malware-analysis pipeline — and every one of them learned about it from a phone call.

A model with no attacker behind it, using techniques from a 2005 penetration test, compromised three companies and stayed undetected until the lab that owned it went looking.

That is not a story about model capability. It is a measurement of the floor.

What to do on Monday

On our own work. Cliff has findings in this component class currently under coordinated disclosure with their vendors. They will appear on the proof page, by class and severity, as the embargoes lift. Nothing in this post is drawn from them — every fact above comes from the public disclosures listed below.