How PythonSkillset Sandboxes Untrusted Code
Learn how PythonSkillset uses a layered approach with AST parsing, subprocess isolation, nsjail containerization, and resource limits to safely execute untrusted Python code on a server.
Don't let untrusted Python code run wild on your server. You might think you can just eval() everything in isolation, but that’s like leaving your front door open and hoping no one walks in. A single line like __import__('os').system('rm -rf /') can demolish your whole system.
Here’s how PythonSkillset handles sandboxing untrusted code safely. We use a layered approach — no single trick is bulletproof alone.
Never trust eval() or exec() alone
The built-in exec() and eval() let you run arbitrary strings as code. But they run in the current environment by default. Give someone exec(user_input) and they have full access to your Python runtime, file system, and network.
Example of what not to do:
def run_code(code):
exec(code) # dangerous
A user could submit:
import subprocess
subprocess.run(["rm", "-rf", "/"])
That’s game over.
Start with exec() with restricted globals and locals
You can limit what’s available by passing custom dictionaries to exec().
def safe_exec(code):
restricted_globals = {"__builtins__": {}}
local_vars = {}
exec(code, restricted_globals, local_vars)
return local_vars
This strips __builtins__, so no open(), import, exec, eval, or __import__. The problem? Crafty users can still recover builtins via introspection. For instance:
().__class__.__bases__[0].__subclasses__()
That chain can eventually lead to os or subprocess. So restricting builtins alone isn’t enough.
Use ast module for parsing before execution
A safer approach is to parse the code into an Abstract Syntax Tree (AST) and reject dangerous patterns before running anything.
import ast
def is_safe(code):
try:
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Attribute):
if node.func.attr in ['__subclasses__', '__bases__', '__globals__']:
return False
elif isinstance(node.func, ast.Name):
if node.func.id in ['exec', 'eval', 'open', 'import']:
return False
return True
except SyntaxError:
return False
But this is still a whack-a-mole game. Attackers find new introspection tricks constantly. You’d need to maintain a long blocklist.
Run code in a subprocess with subprocess or multiprocessing
This isolates the execution at the OS level. The untrusted code runs in a separate process, not in your main application’s memory.
import subprocess
import sys
def run_in_subprocess(code):
proc = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
timeout=5
)
return proc.stdout.decode()
The code can still access the file system and network because the subprocess inherits the same permissions. So you need extra OS-level restrictions.
Use nsjail or docker for true containerization
For PythonSkillset’s production sandbox, we use nsjail — a lightweight, open-source sandbox that uses Linux namespaces. It can limit:
- File system access (read-only or mount a temp directory)
- Network access (blocked by default)
- CPU and memory limits
- Number of processes
Example nsjail command:
nsjail --chroot /sandbox --bindmount /tmp/sandbox:/home --time_limit 5 -- ./python3 -c "code"
Docker is another option, but it's heavier. For short-lived code snippets, nsjail is faster.
Combine CPU and memory limits
Even safe code can be malicious by consuming resources. Use resource module in Python or OS-level cgroups to cap memory and CPU.
import resource
resource.setrlimit(resource.RLIMIT_CPU, (1, 1))
resource.setrlimit(resource.RLIMIT_AS, (100 * 1024 * 1024, 100 * 1024 * 1024))
Set these before running the untrusted code.
The practical PythonSkillset workflow
Here’s the layered approach we follow:
- Parse the code with
astand reject known dangerous patterns. - Execute it in a
subprocesswithnsjailand resource limits. - Set a strict timeout (e.g., 2 seconds).
- Clean up any temporary files after execution.
- Log all inputs and outputs for auditing.
No single layer is perfect, but together they make exploitation very hard. A determined attacker might still find a way, but you’ve raised the bar from “trivial” to “requires serious effort.”
If you’re building a platform where users run code, always assume the worst. Sandboxing isn’t about being 100% secure — it’s about making cost of attack higher than the value of breaking in.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.