Making the executor a real sandbox
The tutorial's executor runs attacker-influenceable code on your machine with your credentials. Here is what has to change before it touches anything you care about.
In 60 seconds
Making the executor a real sandbox
The tutorial's executor runs attacker-influenceable code on your machine with your credentials. Here is what has to change before it touches anything you care about.
run_python_code does one useful thing β it stops a crash taking down the parent. It does not do the thing its name promises. Line it up against what a sandbox actually has to provide:| Property | subprocess | What it means if missing |
|---|---|---|
| Parent survives a crash | β | β |
| Time limit | β (timeout=10) | A runaway loop hangs the agent |
| Filesystem isolation | β | It can read ~/.ssh, ~/.aws, your source |
| Network isolation | β | It can POST your files anywhere |
| Credential isolation | β | It inherits every environment variable you have |
| Memory / CPU limits | β | One line of code exhausts the machine |
| No persistence | β | It can leave a cron entry behind |
The minimum viable real sandbox
import subprocess
import tempfile
import os
IMAGE = "python:3.12-slim"
def run_python_code(code_string, timeout=10):
"""Run untrusted code in a disposable container with no network."""
with tempfile.TemporaryDirectory() as workdir:
path = os.path.join(workdir, "main.py")
with open(path, "w") as f:
f.write(code_string)
try:
result = subprocess.run(
[
"docker", "run",
"--rm", # destroyed when it exits
"--network", "none", # no exfiltration path at all
"--memory", "256m",
"--cpus", "0.5",
"--pids-limit", "64", # blocks fork bombs
"--read-only", # immutable root filesystem
"--tmpfs", "/tmp:size=16m",
"--cap-drop", "ALL",
"--security-opt", "no-new-privileges",
"--user", "65534:65534", # nobody
"-v", f"{workdir}:/work:ro", # only this file, read-only
"-w", "/work",
IMAGE,
"timeout", str(timeout), "python", "main.py",
],
capture_output=True,
text=True,
timeout=timeout + 5,
env={"PATH": os.environ["PATH"]}, # NOT os.environ
)
except subprocess.TimeoutExpired:
return "EXECUTION ERROR:\nTimed out"
if result.returncode != 0:
return f"EXECUTION ERROR:\n{result.stderr[:4000]}"
return f"SUCCESS OUTPUT:\n{result.stdout[:4000]}"Why each flag is there
- 1
--network none
The single highest-value line in the file. With no network interface, generated code cannot exfiltrate anything, no matter what it was told to do. Module 12's whole attack family dies here. - 2
--rm plus a temp directory
Ephemeral. Nothing the code writes, installs or schedules survives the run, so an attack cannot persist into the next user's session (Module 18). - 3
--read-only and --cap-drop ALL
It cannot modify the image or acquire privileges. Combined with running asnobody, a container escape needs a kernel bug rather than a configuration mistake. - 4
env={"PATH": ...}
The quiet one.subprocess.runinherits your entire environment by default β every API key you have exported. Passing an explicit minimal env is a one-line change that removes a whole category of leak. - 5
Output truncation
A model that prints a gigabyte fills your context window and your bill. Cap it.
When containers are not enough
- gVisor or Kata Containers β a syscall boundary between the workload and the host kernel.
- Firecracker microVMs β real virtualisation, boots in ~125ms. What the hosted code-execution services use.
- WebAssembly (Pyodide, Wasmtime) β capability-based by construction: no filesystem or network unless you hand one in. Excellent when the code only has to compute.
- A managed sandbox service β someone else's problem, which is often the correct engineering answer.
The rest of the belt and braces
- A hard cap on iterations, wall-clock time and model spend, enforced outside the loop.
- Every generated code block logged before execution β this is your only forensic record.
- Human approval before the agent may touch anything outside the sandbox.
- A kill switch at the infrastructure layer, not a flag the agent can see (Module 33).
Watch and read more
Lab
A sandbox escape you attempted and failed.
The problem
ATTACKS = {
"read host": "print(open('/etc/passwd').read())",
"network": "import urllib.request; urllib.request.urlopen('http://example.com')",
"memory": "x = bytearray(10**9)",
"fork bomb": "import os\nwhile True: os.fork()",
"persist": "open('/work/persisted.txt','w').write('still here')",
}You are done when
Hard questions
Try to answer before you reveal. If you can answer these, you understood the lesson.
Q1You remove --network none but keep everything else. Rank what an attacker can now do, worst first.Reveal
Questions people ask
Is Docker enough for my internal tool?
For an internal tool where the goals come from your own team, yes, configured as above. The threat model is accident and mistake, not a determined attacker with a kernel exploit.
Can I just filter dangerous code before running it?
You can try, and it is a speed bump (Module 20). There are unlimited ways to express open("/etc/passwd"). Isolation works because it does not depend on recognising the attack.
What about pip install inside the sandbox?
It needs network, which is the thing you just removed. Pre-bake the packages into your image. That is a feature: your agent runs against a known dependency set instead of whatever PyPI serves today.
How much does this slow the loop down?
Container start is roughly 200-500ms. Against a model call of several seconds, it is noise. Keep a warm pool if you ever care.
Lesson test
5 questions. Get 3 right (60%) to pass and complete this lesson.
Sign in with your phone number to take the test and save your progress