all writing
security//3 min read

Escaping the Sandbox: A Technical Retrospective

What breaking out of a container taught me about the gap between isolation models and actual isolation.

There is a version of this story where I tell you about the clever exploit. I am going to tell you the other version: the one where the environment was never as isolated as anyone believed, and the exploit was almost beside the point.

The Setup

The target was a Python sandbox running user-submitted code in a container with dropped capabilities, a read-only filesystem, and network egress blocked at the iptables level. The pitch was that users could run arbitrary code and nothing interesting could happen.

It was a reasonable pitch. The implementation had gaps.

What the Model Got Wrong

Sandboxing is a layered problem. You can get the Linux primitives right and still misconfigure the orchestrator. You can get the orchestrator right and still share kernel state you didn't account for. The gap is rarely in the mechanism you audited; it's in the mechanism you forgot to think about.

In this case, /proc was mounted read-only but not filtered. A few reads in, and the process namespace was visible in ways the sandbox assumed it wasn't.

# what you can learn from an unfiltered /proc
import os
 
with open('/proc/1/cmdline', 'rb') as f:
    init_cmd = f.read().split(b'\x00')
 
with open('/proc/self/cgroup', 'r') as f:
    cgroup_info = f.read()

Neither of these is an exploit. Both told me more than the sandbox intended to share.

The Actual Path Out

The kernel was shared. PID namespaces were scoped correctly, but the cgroup hierarchy leaked information about sibling containers. From there, the question was whether any shared resource could be written, not just read.

It could.

The writeup from that point is a lesson in the difference between "capability dropped" and "capability inaccessible." Dropping CAP_SYS_ADMIN removes a lot of kernel interfaces. It does not remove every interface that requires elevated privilege in a non-namespace-aware kernel.

What Changed After

The fix was not the exploit path. The fix was the audit. Going through every mounted filesystem, every accessible /proc subtree, every cgroup interface: that process uncovered four more misconfigured surfaces that had nothing to do with the original finding.

The lesson I keep returning to: security review should follow the attacker's model, not the defender's mental map. The defender built a system they understand. The attacker walks the system as it actually is.

Those are different systems more often than defenders want to admit.

On Writing These Up

Every retrospective is a constructed narrative. The finding was not sequential; it was a mess of dead ends, wrong guesses, and one moment of noticing something that didn't fit. The clean version of the story is useful for sharing. The messy version is what actually trains intuition.

Write the clean version. Keep the messy notes.