RC RANDOM CHAOS

The sand was never boxed

A sandbox is a set of filters on named dimensions, not a boundary of execution. It matches references and never revalidates the capability they compose.

· 9 min read
The sand was never boxed

A sandbox does not contain a process. It mediates a defined list of operations and permits everything it was never asked to inspect. A seccomp-bpf filter is an enumeration of allowed system calls. A Linux network namespace is a separate routing table and interface set. An egress allow-list expressed as domain names is a set of destinations permitted to pass. In the case under discussion, the contained workload was restricted to contacting specific domain names and was granted write access. Both statements are true at the same time, and both controls were doing exactly what they were configured to do. The egress filter matched names against DNS answers and TLS SNI fields and let the approved ones through. The write grant let bytes land on a surface. Neither control was aware of the other, because neither was designed to be.

The word escape assumes there was a wall. There was not. Each of these mechanisms is a filter across a single dimension. seccomp constrains the syscall surface. The network namespace constrains routing. The domain allow-list constrains outbound naming. None of them is a boundary of execution, and none of them observes what the others permit. Put together, they do not compose into isolation. They compose into a list. The sand was never boxed. Specific grains were labeled, counted, and waved through, and the space between the labeled grains was never part of the model.

What the sandbox produced was an artifact of constraint. A report that egress is filtered, that the namespace is in place, that writes are scoped to a path. That report measures the presence of controls on named dimensions. It does not measure the set of outcomes reachable by combining them. A green state means the enumerated filters are installed and matching, not that the union of everything they allow is bounded. This is the first thing to hold onto: the sandbox was never an isolation mechanism. It was a proxy for control, an inventory of permitted operations standing in for a boundary that was never actually drawn.

The assumption underneath this design is that capability equals the sum of individually enumerated permissions. Constrain egress to domains A, B, and C, grant write to path P, and the resulting capability is understood to be exactly that: can reach A, B, and C, can write to P, and nothing beyond it. The trust model is compositional and additive. It treats isolation as an emergent property of deny-by-default, as if enumerating the permitted operations and refusing the rest were the same as bounding what the process can accomplish. Each grant was evaluated once, on its own, at configuration time, against the question of whether that single channel was acceptable in isolation.

The allow-list of domain names carried a second assumption inside it. It treated a domain as a boundary of communication when a domain is only a boundary of naming. A name is a label resolved to an address by DNS, a system that sits entirely outside the sandbox’s trust perimeter, and validated against an SNI string the client itself presents. The list assumed that the mapping from name to destination is stable and honest, that an approved domain is a fixed and benign endpoint, and that contacting it is a leaf operation rather than a composite one that includes whatever that name resolves to and whatever content returns over the permitted channel. The write grant carried the mirror of that assumption: that a location is a boundary, that bytes written there are inert, and that the surface is not also read by something else with more reach.

Trust in this model is persistent and transferable. Once a domain is on the list, it is trusted for the life of the process, across every request, regardless of what it returns or who controls the address behind it now. Once write access is granted, it is trusted regardless of what is written or what later consumes it. The two decisions were made separately, held statically, and never reconciled with each other. That was acceptable as long as one condition held: that the permitted channels could not be combined into something larger than any of them individually. The whole architecture rests on that condition, and the architecture never checks it.

What changed was not attacker capability, and it was not a defect in enforcement. What changed was the standing of the assumption that enumerated permissions bound outcomes. The composition of egress to specific domains and write access to a shared surface is not the intersection of two constraints. It is a channel. A permitted write can land on a location that a permitted egress destination later reads. A permitted domain can resolve to an address the operator does not control, or return content that the write surface acts upon. The capability that results is larger than either grant, and it was present the moment both grants coexisted. Nothing had to break for it to exist. The two controls simply continued to enforce, in parallel, the exact things they were told to enforce.

The domain allow-list resolves through DNS and TLS SNI, mechanisms the sandbox references but does not own. The premise that an approved name means a fixed, benign destination was never a structural guarantee. A name can point somewhere new. An approved domain can host content chosen by someone else. A write path can be a shared volume, an object store, or a build artifact directory that a more privileged process reads without re-checking its origin. None of this required the sandbox to malfunction. Through all of it, the egress filter kept matching the approved names, and the write grant kept accepting bytes on the approved path. Each control stayed green while the space between them widened into something usable.

The system did not re-evaluate the composition, because it was never built to. It inherited the trust decisions made at configuration time and carried them forward unexamined. It enforced references, the name on the list and the path in the grant, not the integrity of what those references combined into. To say the assumption no longer holds is too generous. It never held. The gap between the sum of the permissions and the set of reachable outcomes was there from the first line of the policy. What changed is only that the gap became reachable, and the sandbox, resolving each reference exactly once and never again, had no mechanism that could notice.

The failure is not in enforcement. It is in what enforcement checks. The egress filter checks a reference: the domain name carried in the TLS SNI field and returned by DNS. It does not check what that name resolves to at the moment the connection opens, and it does not check what returns across the connection once the name has matched. Matching the name is treated as equivalent to validating the destination. The reference stands in for the thing it names. An X.509 certificate presented during the TLS handshake attests that a party controls a name. It does not attest to the integrity or the intent of the bytes served under that name. The allow-list consumes the first attestation and infers the second, which no part of the protocol ever offered.

The write grant works the same way. It checks a reference, a path, and accepts bytes because the location is on the approved surface, not because of what the bytes are or what will later read them. The identity of the destination stands in for the integrity of the content. From outside, the observable behaviour is unremarkable. A connection to an approved name completes. A write to an approved path succeeds. Those two events look identical whether the content is inert or the composition is being used as a channel. The sandbox emits the same state in both cases, because the dimensions it observes have not moved. The name matched. The path matched. Nothing it measures changed.

This is the shape of every control that authenticates a source and infers the safety of what the source delivers. The system executed exactly the operations it was configured to permit. No rule was evaded. No boundary was crossed by force. The capability formed inside the permitted set: a permitted write lands where a permitted egress destination reads, a permitted name resolves to an address the operator does not hold, permitted content returns over a channel approved by its label. The controls kept matching references. What they never did, because it was never in their model, was revalidate the composition those references produced. Validation happened once, at the level of the name and the path, and was then inherited forward as though it had been performed on the outcome.

Strip the sandbox away and a general form remains. A system permits an operation because a reference matches, and treats the match as if it were a verification of the outcome the operation produces. The reference is cheap to check and stable to store: a name, a path, a version string, a token. The outcome is expensive to verify and changes over time. So the system checks the reference once and carries the result forward. Execution follows the reference. Verification, where it happened at all, happened earlier, against a narrower question, and is not repeated.

The same mechanism runs through OAuth 2.0 bearer tokens, defined in RFC 6750. A bearer token is a reference to a prior authentication. The resource server grants access on presentation of the token, matching it the way the egress filter matches a name. It does not re-verify the principal at the moment of the request. Possession of the reference is accepted as the right to act. Whoever holds the token holds the access, regardless of how they came to hold it. The token points at a past state in which identity was established, and the server executes against that reference, not against a present check of who is calling. The structure is the same as the allow-list. An approved name, an approved path, an approved token, each honored for matching, none re-examined for what it now permits.

In every one of these cases the system is not deceived and nothing malfunctions. The bearer token is valid. The domain is on the list. The write path is approved. The gap is structural. The reference was never the same object as the outcome, and the system was built to check the reference. Trust is delegated to the artifact. The token, the name, and the path become the objective. Control what the reference points to, or what the referenced surface later feeds, and you inherit the trust the system placed in the reference, without ever touching the check.

A sandbox is a set of filters on named dimensions. It is not a boundary of execution. Isolation was the word used for it, not the property it held.

The system resolves each reference once. The name against the list, the path against the grant. It does not revalidate the capability those references compose, because that capability was never something it could see.

There was no escape, because there was no wall. The sand was labeled, not boxed. The control exists. The outcome does not.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.