How to run untrusted code safely
"Untrusted" now usually means "written by a language model thirty seconds ago", which is a different threat from a malicious human but lands in the same place: code you did not read, doing something you did not predict, on a machine you care about. Here is what each of the common answers actually stops.
The options, from weakest boundary to strongest
| Approach | Stops | Does not stop | Cost |
|---|---|---|---|
| Language-level restriction stripped builtins, restricted import | Casual mistakes and a naive script. | A determined escape, anything that spawns a subprocess, filesystem and network access. | Nothing, and a false sense of safety. |
| Separate process + rlimits seccomp, cgroups, a dedicated user | Runaway memory and CPU, most accidental damage. | Kernel-level escapes, reading anything the user can read, unrestricted egress. | Low, but you own every rule. |
| Container Docker, containerd | Filesystem and process visibility, resource limits, most accidents. | A shared-kernel escape. One kernel bug reaches the host and its neighbours. | Low, plus a daemon to operate. |
| Sandboxed kernel gVisor, Kata | Most of what a container misses, by intercepting syscalls. | Some workloads outright — the syscall surface is deliberately incomplete. | Moderate; a real performance cost on syscall-heavy work. |
| microVM Firecracker, Cloud Hypervisor | Shared-kernel escapes. Each guest has its own kernel. | Nothing about egress. You still own the abuse controls. | Moderate, and a real amount of orchestration to build. |
| A whole machine per execution | Everything above, plus any doubt about what the previous tenant left behind. | Nothing about egress, again. Startup is measured in tens of seconds, not milliseconds. | Highest per hour, lowest per unit of reasoning about it. |
There is no row here that is correct for everyone. The question that decides it is not "how dangerous is this code" but "what is on the other side of the boundary if it gets out".
The mistake almost everyone makes first
The instinct is to reach for the strongest isolation and stop there. But isolation is only half the problem, and it is the half that protects you. The other half is what the code does with the network you handed it.
A sandbox with unrestricted egress is a machine on the internet, running code nobody read, with your name on the IP address. It can scan, it can send mail, it can join someone else's attack. The isolation worked perfectly and you still get the abuse report — and, in the worst case, the account suspension that follows it. Whatever you build or buy, the questions worth asking are:
- Is the cloud metadata endpoint reachable? It is the shortest path from "runs code" to "has your credentials".
- Is outbound SMTP blocked? Nothing legitimate in a sandbox needs to send mail directly.
- Are private address ranges unreachable, so a sandbox cannot reach your other infrastructure?
- Is there a rate limit on new outbound connections, so a port scan fails rather than completes?
- Does the code run as root? On a machine of its own that is survivable; it is still an unnecessary gift.
We publish our answers to all five, and the things we deliberately do not claim, on the security page.
Design the lifetime before you design the sandbox
The failure that actually costs money is not an escape, it is a leak: sandboxes that were created and never destroyed, quietly billing. Any design here needs an answer to "what happens when the process that was supposed to clean this up crashes", and the only durable answer is a deadline that does not depend on your code running.
Ours is a timeout set at creation — 15 minutes by default, 24 hours at
most — enforced by the platform whether or not anything calls DELETE, with a
reconciler that destroys machines whose bookkeeping went missing. How
that works is written up in full. If you build your own, budget as much time for the reaper
as for the sandbox.
Doing it with an API instead
Write the file, run it, read what it produced, throw the machine away:
SBX=$(curl -sS -X POST https://sandbox-as-a-service.com/v1/sandboxes \
-H "Authorization: Bearer $AAS_API_KEY" -H "Content-Type: application/json" \
-d '{"size":"small","timeout_minutes":10}' | jq -r .id)
curl -sS -X PUT https://sandbox-as-a-service.com/v1/sandboxes/$SBX/files \
-H "Authorization: Bearer $AAS_API_KEY" -H "Content-Type: application/json" \
-d '{"path":"submission.py","content":"print(sum(range(10)))"}'
curl -sS -X POST https://sandbox-as-a-service.com/v1/sandboxes/$SBX/exec \
-H "Authorization: Bearer $AAS_API_KEY" -H "Content-Type: application/json" \
-d '{"command":"timeout 30 python3 submission.py"}'
curl -sS -X DELETE https://sandbox-as-a-service.com/v1/sandboxes/$SBX \
-H "Authorization: Bearer $AAS_API_KEY"import os, requests
API = "https://sandbox-as-a-service.com/v1"
H = {"Authorization": f"Bearer {os.environ['AAS_API_KEY']}"}
def run_untrusted(source: str, timeout_s: int = 30) -> dict:
sbx = requests.post(f"{API}/sandboxes", headers=H,
json={"size": "small", "timeout_minutes": 10}).json()
try:
requests.put(f"{API}/sandboxes/{sbx['id']}/files", headers=H,
json={"path": "submission.py", "content": source}).raise_for_status()
return requests.post(f"{API}/sandboxes/{sbx['id']}/exec", headers=H,
json={"command": f"timeout {timeout_s} python3 submission.py"}).json()
finally:
# The timeout would get it anyway; this returns the minutes you did not use.
requests.delete(f"{API}/sandboxes/{sbx['id']}", headers=H)
Note the inner timeout: the sandbox lifetime protects your bill, but a per-command
limit is what stops one submission holding the machine for the full ten minutes.
Questions
Is a Docker container enough to run untrusted code?
It depends what you are defending against. A container is a strong boundary against accident and a weaker one against intent: the kernel is shared, so a kernel bug is a path from a container to the host and to everything else on it. That is why the people who run untrusted code at scale — CI providers, notebook hosts, code-execution APIs — put a virtualisation boundary underneath, whether that is a microVM, gVisor, or a whole machine. If the code is yours and you are sandboxing against your own mistakes, a container is proportionate. If a stranger or a language model wrote it, it is not the boundary you want alone.
What about restricting the language runtime instead?
Stripping eval, patching __builtins__ or running with a restricted importer is the cheapest option and the one most often defeated. Python in particular has a long history of escapes from home-made sandboxes, because the object graph is reachable in ways that are difficult to enumerate. Language-level restriction is useful for shaping an API surface; it is not a security boundary against an adversary, and it is worth nothing against a subprocess that shells out.
Does isolation protect me from what the code sends over the network?
Not by itself, and this is the part people forget. Isolation stops the code reaching you; it does nothing about the code reaching everyone else. A sandbox with unrestricted egress can scan, spam or attack from your account's address, and the bill for that arrives as an abuse complaint. Look for outbound controls specifically: cloud metadata blocked, SMTP blocked, private ranges unreachable, and a connection rate limit. Ours are listed on the security page, including what we do not claim.
How much does a dedicated machine per execution actually cost?
Less than most people assume, because the machine only exists while the work does. At $0.09 an hour for 2 vCPU and 4 GB, a two-minute run is $0.003. A thousand of them a month is $3.00. The cost that matters at that volume is not the machine, it is the startup latency, which is why the sensible design is one sandbox per session rather than per call where you can.
What is the fastest way to get something working?
Create a sandbox, write the file, run it, read the result, destroy it — five HTTP calls with no infrastructure to operate. The quickstart is copy-pasteable and there is $5 of credit on signup, which is about 56 hours. If you would rather run it yourself, Firecracker and gVisor are both open source; the work is not the sandbox, it is the lifecycle, the reaping and the abuse controls around it.
Run it somewhere you can throw away
A dedicated VM per execution, destroyed on a deadline you set. $5 of credit on signup, no card.