Wave 6 — Cloudflare Containers exec() probe family (W-CTR-01…11) - #10
Merged
Conversation
Deploy-verified all seven W-WC probes against wulf-worker.sharted.workers.dev (version 4fd485f8-98ae-44ff-8cb7-d9a9499dcc30). W-WC-04 confirmed critical: the identity header is not part of the cache key, so one caller receives another caller's per-user secret. Wiring: - Uncomment the SVC_WC_VICTIM loopback service binding. - Enable Workers Cache on the WcVictim entrypoint only, keeping the /probe/* router uncached so existing Track-1 probes stay deterministic. Syntax verified against wrangler 4.108.0: `exports` is a map keyed by export name and each entry requires type = "worker" (not an [[exports]] array with a name key). Probe corrections found during deploy-verify: - W-WC-02: the post-purge re-read raced purge propagation and always reported "not evicted". Added a 10s propagation wait before the re-read; no verdict derives from elapsed time, so the nonce oracle stays host-clock-independent. - W-WC-05: split into two arms. The Authorization auto-bypass is decided on the inbound request, so sending it masked the heuristic-freshness test entirely. Arm A (no Authorization) exercises heuristic freshness; arm B keeps it as a control. Arm B refutes the "defeated auto-bypass" thesis for Authorization — the bypass held despite the Worker stripping the header from the response. - /wc/sensitive now emits a w-wc-05 canary instead of w-wc-04's. Remaining gap: the cross-PoP amplification half of W-WC-06/07 still needs an external multi-region driver. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pinned driver The cross-PoP half of W-WC-06/07 never needed an external multi-region driver. A Durable Object is the only primitive that lets Worker code choose where it runs: locationHint pins a new object to a region, and a service-binding fetch from inside it dispatches in that object's own data center. WcRegionDriver drives SVC_WC_VICTIM from objects pinned to wnam/weur/apac and reports the colo each one actually ran in (read back from /cdn-cgi/trace), so a conclusion is only drawn from confirmed-different colos. Observed: DFW/AMS/KIX, DEN/MRS/SIN, LAX/CDG/HKG, SJC/LIS. The object stores nothing; a DO class still needs a migration, hence the first [[migrations]] tag on wulf-worker. Both cross-PoP results are defensive, and both bound blast radius: - W-WC-06 amplification REFUTED. Remote colos did not serve the origin's nonce, and on a 15s re-read each returned the nonce it had created itself as a HIT — every data center holds its own entry, which excludes propagation lag as the explanation. Verified by hand first (same run_id 45s apart), then encoded as the probe's own `per-colo` verdict so it no longer rests on a hand-run. This downgrades the practical reach of W-WC-03: a poisoned entry does not go global for free. Kept as a regression — it flips to `confirmed` if tiering changes. - W-WC-07 collapsing is bounded to one data center. Simultaneous 8-way bursts from SJC and LIS produced disjoint nonce sets, so victim-joining is capped at one PoP's concurrent traffic. W-WC-05 arm C closes the Set-Cookie half: re-attaching Set-Cookie to the shape arm A already proved cacheable yields BYPASS, so the auto-bypass is real and it is the Worker's response-side strip that destroys it. The two halves of the thesis have opposite answers — Authorization is decided inbound and cannot be defeated response-side (arm B), Set-Cookie can. Deploy-verified on version bd8f2395-a5a3-4cd1-9997-1cd1e67fe0b2; raw results in packages/harness/results/w-wc-0*-wave5-xpop-bd8f2395.json. W-WC-01..04 re-run unchanged. Wave 5 has no open items left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ily (Wave 6) Cloudflare Containers shipped `ctx.container.exec()`, letting Worker code hand attacker-controlled bytes and attacker-influenced argv to a real Linux process — including streaming a request body straight into stdin. That is a different trust boundary from every existing track: full POSIX, a writable filesystem, its own egress path, and a lifetime decoupled from the request. On Lambda/Azure/GCP the container *is* the function sandbox, so there is no isolate->container process API to attack; that framing is the comparison block on all 11 probes. New package @wulf/container: the `CtrTarget` container DO exposing a deliberately vulnerable JSON-RPC surface over ctx.container (argv exec, sh -c interpolation, request-body->stdin, detached spawn, egress, snapshots, interception, getTcpPort), plus an InterceptSink entrypoint. Driven from wulf-worker over SVC_CTR. Deploy-verified 2026-07-30 (run wave6-20260730): 10 accessible, 1 blocked. - W-CTR-02 (high): argv is literal; the same string via sh -c executes as root. - W-CTR-04 (high): exec'd processes get a clean env, but /proc/1/environ yields the secret canary plus Cloudflare's DURABLE_OBJECT_ID/DEPLOYMENT_ID/PLACEMENT_ID. - W-CTR-03: byte-exact stdin incl. chunked with no Content-Length, a real WebP from ffmpeg, and a payload that selected the concat demuxer on auto-probed input. - W-CTR-05/06/07/11: raw TCP beyond fetch()'s reach, processes outliving the request, root with full caps and a writable /usr/bin, exec-opened ports reachable. - Countervailing: enableInternet:false binds exec'd processes, output() enforces a real memory ceiling, HTTPS interception fails cert validation. W-CTR-09 blocked — the snapshot API is typed and callable but returns internal errors. wulf-container is gated by a shared secret (WULF_CTR_AUTH) in addition to workers_dev=false: the first deploy enabled a workers.dev route by default and the arbitrary-sh -c endpoint was briefly reachable from the internet. Routing is not an access control for that Worker. Full results, per-probe rationale, safety rails and the corrections made mid-run (a destroy() that wedged a shared DO, a TCP classifier that missed a successful connect, a pkill cleanup that killed itself) are in docs/wave6-containers-exec.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New probe family targeting Cloudflare Containers
ctx.container.exec()— the API that lets Worker code hand attacker-controlled bytes and attacker-influenced argv to a real Linux process, including streaming a request body straight into stdin.This is a different trust boundary from every existing track: full POSIX, a writable filesystem, its own egress path, and a lifetime decoupled from the request. On Lambda / Azure Functions / GCP the container is the function sandbox, so there is no isolate→container process API to attack — that framing is the
comparisonblock on all 11 probes.packages/container/(new@wulf/container) —wulf-containerWorker owning theCtrTargetcontainer DO: a deliberately-vulnerable JSON-RPC surface overctx.container(argv exec,sh -cinterpolation, request-body→stdin, detached spawn, egress, snapshots, interception,getTcpPort), plus anInterceptSinkentrypoint. Mirrors thepackages/llamafirewalllayout.W-CTR-01…11inpackages/worker, driven over theSVC_CTRservice binding, with_ctr.tsas the shared client. Optional binding → every probe reportsblockedlocally / pre-deploy.docs/wave6-containers-exec.md— rationale, architecture, safety rails, per-probe roster, deploy runbook, full results.Deploy-verify (2026-07-30, run
wave6-20260730)Image 548 MB,
instance_type = "basic", coloiad21. 10accessible, 1blocked.Seccomp: 0,CapEff: 000001ffffffffff,NoNewPrivs: 0.inspect()/interceptOutboundTcp()typed but not implemented.sh -cexecutes and returnsuid=0(root).Content-Length), realRIFF…WEBPfrom ffmpeg, and the payload selected the concat demuxer on auto-probed input — ffmpeg's-safedefault refused the local path./proc/1/environyields the secret canary plusCLOUDFLARE_DURABLE_OBJECT_ID/DEPLOYMENT_ID/PLACEMENT_ID/LOCATION.fetch()'s reach). No link-local / foreign RFC1918.enableInternet: falsedoes bind exec'd processes./usr/binwritable,useroption not honoured by name./dev+ initial userns show this is a microVM guest — the VM, not the container, is the boundary.Memory limit exceeded before EOF.snapshotDirectory/Container()callable but return internal errors — typed and exposed, not functional.exec()and declared nowhere is reachable viagetTcpPort(); dead port correctly refused.The centre of gravity is the pairing: W-CTR-02 supplies code execution from request data, and 04/05/06/07/11 each price it. Three results cut the other way and are stated as plainly — the containment story is better than the capability inventory suggests.
Security note
wulf-containeris gated by a shared secret (WULF_CTR_AUTH,x-wulf-ctr-auth) in addition toworkers_dev = false. The first deploy enabled aworkers.devroute by default and the arbitrary-sh -cendpoint was confirmed executing a command from an unauthenticated public request; it was closed immediately. Routing is not an access control for that Worker.Notes for review
destroy()that wedged a shared DO into serialized-request timeouts, a TCP classifier that missed a successful connect, apkill -fcleanup that killed itself, an "RFC1918" target that was the container's own address, an interception credited to a failed install). All documented in the wave doc's Probe corrections section; results above are from the re-run.docker build --network=host— on the WSL2 host the default Docker bridge resolvesdeb.debian.orgAAAA-only with no IPv6 route.pnpm -r typecheckclean; deployed registry lists 81 probes, 11 of themW-CTR-*.packages/harness/results/(gitignored, per repo convention).🤖 Generated with Claude Code