How-tos

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.

July 2026 5 min read 9 views 0 hearts

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:

  1. Parse the code with ast and reject known dangerous patterns.
  2. Execute it in a subprocess with nsjail and resource limits.
  3. Set a strict timeout (e.g., 2 seconds).
  4. Clean up any temporary files after execution.
  5. 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.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.