Skip to main content

Minisandbox for Python interpreter.

Project description

DCNR Minisandbox

A secure, lightweight Python sandbox interpreter that allows safe execution of untrusted Python code with strict limitations, plus a global object registry and LPC (Local Procedure Call) system for application data management and anonymous method execution.

Features

  • Secure AST-based execution - Uses Python's AST parser to validate and execute code without using eval() or exec()
  • Restricted syntax - Only allows a safe subset of Python constructs
  • Built-in safety limits - Prevents infinite loops with iteration limits
  • Customizable environment - Pre-populate variables and control available functions
  • Comprehensive error handling - Clear error messages for syntax and runtime violations
  • Global object registry - Hierarchical storage and access system for application objects and data
  • LPC (Local Procedure Call) - Anonymous execution of registered methods by name

Installation

pip install dcnr-minisandbox

Components

This package provides three main components:

  1. Sandbox Interpreter (SandboxInterpreter, sandbox_exec) - Secure execution of Python code with AST validation
  2. Object Registry (get_registry, register_object) - Global hierarchical storage for application objects
  3. LPC (Local Procedure Call) (register_proc, exec) - Anonymous execution of registered methods

Quick Start

Sandbox Execution

The sandbox can be used either via the convenience function sandbox_exec or directly through the SandboxInterpreter class.

from dcnr.minisandbox import sandbox_exec

# Simple calculation
code = """
x = 10
y = 20
result = x + y * 2
"""

variables = {}
result = sandbox_exec(code, variables)
print(result)  # {'x': 10, 'y': 20, 'result': 50}

# Pre-populate with variables
initial_vars = {'data': [1, 2, 3, 4, 5]}
code = """
total = sum(data)
average = total / len(data)
"""

result = sandbox_exec(code, initial_vars)
print(result)  # {'data': [1, 2, 3, 4, 5], 'total': 15, 'average': 3.0}

# Using attribute access and method calls
code = """
text = "hello world"
upper_text = text.upper()
words = text.split()
data = [1, 2, 3]
data.append(4)
"""

result = sandbox_exec(code, {})
print(result)  # {'text': 'hello world', 'upper_text': 'HELLO WORLD', 'words': ['hello', 'world'], 'data': [1, 2, 3, 4]}

Global Object Registry

from dcnr.minisandbox import register_object, get_registry

# Register functions and data
def fetchone():
    return {"id": 1, "name": "Alice"}

register_object("database.prod.fetchone", fetchone)
register_object("database.prod.version", "1.0")
register_object("config.debug", True)

# Access through registry
registry = get_registry()
print(registry.database.prod.fetchone())  # {'id': 1, 'name': 'Alice'}
print(registry.database.prod.version)     # '1.0'
print(registry.config.debug)              # True

LPC (Local Procedure Call)

from dcnr.lpc import register_proc, exec

# Register procedures
def calculate_sum(a, b):
    return a + b

def format_message(name, age):
    return f"{name} is {age} years old"

register_proc("math.sum", calculate_sum)
register_proc("string.format", format_message)

# Execute procedures anonymously
result = exec("math.sum", 10, 5)
print(result)  # 15

message = exec("string.format", "Alice", 30)
print(message)  # "Alice is 30 years old"

# Using decorator syntax
from dcnr.lpc import lpc_proc

@lpc_proc("math.multiply")
def multiply(x, y):
    return x * y

result = exec("math.multiply", 4, 7)
print(result)  # 28

Allowed Features

Data Types

  • Numbers: int, float
  • Strings: str
  • Collections: list, tuple, dict, set
  • Booleans: bool

Operators

  • Arithmetic: +, -, *, /, //, %, **
  • Comparison: ==, !=, <, <=, >, >=, in, not in, is, is not
  • Logical: and, or, not

Control Flow

  • Conditional statements: if, elif, else
  • Loops: for, while (with iteration limits)
  • Loop control: break, continue

Built-in Functions

  • Math: abs(), min(), max(), sum()
  • Type conversion: int(), float(), str(), bool()
  • Collections: len(), range(), list(), tuple(), dict(), set(), sorted()

Variable Operations

  • Assignment: x = value
  • Augmented assignment: x += 1, x -= 1, etc.
  • Tuple/list unpacking: a, b = (1, 2)
  • Subscript access: data[0], data[1:3]
  • Attribute access: obj.attr, obj.method()

Prohibited Features

For security reasons, the following are not allowed:

  • Imports: No import or from ... import statements
  • Function definitions: No def, lambda, or class definitions
  • Dangerous attribute access: No access to __class__, __dict__, __module__, etc.
  • Exception handling: No try, except, finally, raise
  • File I/O: No file operations or system calls
  • Advanced features: No comprehensions, generators, decorators, or async code

Global Object Registry for storing and accessing application objects, functions, and configuration data.

Registry Features

  • Dot-notation access - Navigate the registry like registry.database.prod.version
  • Safe path validation - Prevents conflicts with Python keywords and reserved names
  • Singleton pattern - Single global registry instance across your application
  • Type safety - Automatic wrapping of nested dictionaries for consistent access

Registry API

from dcnr.minisandbox import get_registry, register_object

# Get the global registry instance
registry = get_registry()

# Register objects at various paths
register_object("app.config.debug", True)
register_object("app.database.host", "localhost")
register_object("app.database.port", 5432)

# Alternative: use registry methods directly
registry.register_object("services.auth.enabled", True)

# Access registered objects
print(registry.app.config.debug)      # True
print(registry.app.database.host)     # 'localhost'
print(registry.services.auth.enabled) # True

# Check if path exists
if registry.has_path("app.config.debug"):
    print("Debug mode is configured")

# Get object programmatically
db_host = registry.get_object("app.database.host")

# Remove objects
registry.unregister_object("app.config.debug")

# Clear entire registry
registry.clear()

Path Restrictions

For safety and consistency, certain path components are not allowed:

# ❌ These will raise ValueError:
register_object("app.items.test", 1)      # 'items' is reserved (dict method)
register_object("app.__class__.x", 1)     # Names starting with '_' not allowed
register_object("app.for.x", 1)          # 'for' is a Python keyword
register_object("app..x", 1)             # Empty components not allowed

SandboxInterpreter Class

For advanced use cases, use SandboxInterpreter directly. This gives you full control over the interpreter lifecycle, debug logging, and the ability to inject callable functions that interact with the sandbox environment.

Basic Class Usage

from dcnr.minisandbox import SandboxInterpreter

interp = SandboxInterpreter(variables={"x": 10, "y": 5})
env = interp.execute("result = x + y")
print(env["result"])  # 15

# Re-use the same instance for multiple executions
# (environment accumulates across calls)
interp.execute("z = result * 2")
print(interp.env["z"])  # 30

Debug Mode

SandboxInterpreter has a built-in debug buffer that records every statement, its input values and its result in execution order.

from dcnr.minisandbox import SandboxInterpreter

interp = SandboxInterpreter(debug=True)
interp.execute("""
x = 4
y = x * 3
items = [1, 2, 3]
items.append(y)
""")

print(interp.get_debug_log())

Example output:

[Constant]
  => 4
[Assign.Name]
  name: 'x'
  => 4
[Assign]
  targets: ['x']
  value: 4
  => 4
[BinOp]
  op: 'Mult'
  left: 4
  right: 3
  => 12
...

Debug mode can also be toggled at runtime and the buffer cleared between executions:

interp = SandboxInterpreter()         # debug off by default
interp.set_debug(True)
interp.execute("a = 1 + 2")
log = interp.get_debug_log()
interp.clear_debug_log()              # reset buffer for next run

Registering Callable Functions in the Environment

Any callable placed in the variables dict is directly callable from sandbox code by its key name. This is the primary mechanism for extending the sandbox with host-side logic.

from dcnr.minisandbox import SandboxInterpreter

def fetch_price(product_id):
    prices = {1: 9.99, 2: 14.99, 3: 4.49}
    return prices.get(product_id, 0.0)

interp = SandboxInterpreter(variables={"fetch_price": fetch_price})
env = interp.execute("""
price = fetch_price(2)
total = price * 3
""")
print(env["total"])  # 44.97

Writing Back to the Sandbox Environment from a Registered Function

A registered function can write values directly into interp.env so those values appear as normal variables in the dict returned by execute(). The cleanest approach is to close over interp.env using a factory function:

from dcnr.minisandbox import SandboxInterpreter

def make_env_writer(env):
    """Return a callable the sandbox can call to write a named variable."""
    def set_var(name, value):
        env[name] = value
    return set_var

interp = SandboxInterpreter()
# Register after creation so the closure captures the live env dict
interp.env["set_var"] = make_env_writer(interp.env)

env = interp.execute("""
set_var("status", "done")
set_var("count", 42)
""")

print(env["status"])  # "done"
print(env["count"])   # 42

A more elaborate example — a host-side function that fetches a record and writes multiple result variables back into the sandbox:

from dcnr.minisandbox import SandboxInterpreter

def make_db_fetch(env):
    database = {
        1: {"name": "Alice", "score": 95},
        2: {"name": "Bob",   "score": 82},
    }
    def db_fetch(record_id):
        record = database.get(record_id)
        if record:
            for key, value in record.items():
                env[key] = value      # each field becomes a sandbox variable
            env["found"] = True
        else:
            env["found"] = False
    return db_fetch

interp = SandboxInterpreter(variables={"user_id": 1})
interp.env["db_fetch"] = make_db_fetch(interp.env)

env = interp.execute("""
db_fetch(user_id)
if found:
    result = name + " scored " + str(score)
""")

print(env["result"])  # "Alice scored 95"

TrackedDict

TrackedDict is a dict subclass that records which keys were written or deleted since the last clear_updates() call. Pass it as the variables argument to observe exactly what the sandbox script produced or modified, without inspecting the whole environment.

from dcnr.minisandbox import SandboxInterpreter, TrackedDict

initial = TrackedDict({"x": 10, "y": 5})
interp = SandboxInterpreter(variables=initial)
env = interp.execute("""
result = x + y
label  = "sum"
""")

# Only keys written during execution are reported
for key, value in env.updated_items():
    print(f"  {key} = {value!r}")
# result = 15
# label  = 'sum'

print(env.deleted_keys())  # []

Resetting the Tracker Between Runs

Call clear_updates() before each execute() call to get only the changes from that specific run:

env.clear_updates()
interp.execute("z = result * 2")

new_writes = dict(env.updated_items())
print(new_writes)  # {'z': 20}

Combining TrackedDict with a Registered Writer Function

TrackedDict works transparently with the env-writer pattern — writes made by the registered function are tracked just like writes made by sandbox statements:

from dcnr.minisandbox import SandboxInterpreter, TrackedDict

tracked = TrackedDict()
interp = SandboxInterpreter(variables=tracked)
interp.env["set_var"] = lambda name, value: tracked.__setitem__(name, value)

interp.execute("""
set_var("api_result", 42)
total = api_result * 2
""")

print(dict(tracked.updated_items()))
# {'api_result': 42, 'total': 84}

TrackedDict API Reference

Method Description
updated_items() Generator of (key, value) for every key written since last clear_updates()
deleted_keys() List of keys deleted since last clear_updates()
clear_updates() Reset both the written and deleted sets

LPC (Local Procedure Call)

The LPC package provides anonymous execution of registered callable methods, enabling dynamic procedure dispatch without direct references to the implementation.

LPC Features

  • Anonymous execution - Call procedures by name without importing or referencing them directly
  • Dynamic registration - Register procedures at runtime from any part of your application
  • Decorator support - Use @lpc_proc decorator for clean registration syntax
  • Type safety - Validates that registered objects are callable
  • Error handling - Clear exceptions for missing procedures or execution failures
  • Procedure management - List, check, and remove registered procedures

Core API

Registration Functions

from dcnr.lpc import register_proc, lpc_proc

# Function registration
def add_numbers(a, b):
    return a + b

register_proc("math.add", add_numbers)

# Decorator registration
@lpc_proc("math.subtract")
def subtract_numbers(a, b):
    return a - b

# Lambda registration
register_proc("math.square", lambda x: x ** 2)

Execution Function

from dcnr.lpc import exec

# Execute registered procedures
result1 = exec("math.add", 10, 5)        # 15
result2 = exec("math.subtract", 10, 5)   # 5
result3 = exec("math.square", 4)         # 16

# With keyword arguments
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

register_proc("utils.greet", greet)
message = exec("utils.greet", "Alice", greeting="Hi")  # "Hi, Alice!"

Management Functions

from dcnr.lpc import has_proc, list_procs, proc_count, unregister_proc, clear_procs

# Check if procedure exists
if has_proc("math.add"):
    print("Math add procedure is available")

# List all registered procedures
procedures = list_procs()
print("Available procedures:", procedures)

# Get procedure count
count = proc_count()
print(f"Total procedures: {count}")

# Remove specific procedure
unregister_proc("math.add")

# Clear all procedures
clear_procs()

Advanced Usage

Procedure Pipelines

from dcnr.lpc import register_proc, exec

# Register data processing pipeline
register_proc("data.load", lambda: [1, 2, 3, 4, 5])
register_proc("data.filter_even", lambda data: [x for x in data if x % 2 == 0])
register_proc("data.sum", lambda data: sum(data))

# Execute pipeline
raw_data = exec("data.load")
filtered_data = exec("data.filter_even", raw_data)
result = exec("data.sum", filtered_data)
print(result)  # 6 (2 + 4)

Conditional Procedure Execution

from dcnr.lpc import exec, has_proc

def safe_exec(proc_name, *args, **kwargs):
    if has_proc(proc_name):
        return exec(proc_name, *args, **kwargs)
    else:
        print(f"Procedure '{proc_name}' not available")
        return None

# Safe execution
result = safe_exec("math.add", 1, 2)  # Works if registered
result = safe_exec("missing.proc", 1, 2)  # Prints warning, returns None

Dynamic Procedure Loading

from dcnr.lpc import register_proc, exec

# Dynamic loading based on configuration
config = {
    "processors": ["data.clean", "data.transform", "data.validate"]
}

def clean_data(data):
    return [x for x in data if x is not None]

def transform_data(data):
    return [x * 2 for x in data]

def validate_data(data):
    return all(isinstance(x, (int, float)) for x in data)

# Register based on configuration
processors = {
    "data.clean": clean_data,
    "data.transform": transform_data,
    "data.validate": validate_data
}

for proc_name in config["processors"]:
    if proc_name in processors:
        register_proc(proc_name, processors[proc_name])

# Execute processing chain
data = [1, None, 3, 4.5]
for proc_name in config["processors"]:
    data = exec(proc_name, data)
print(data)  # [2, 6, 9.0] (cleaned, transformed, validated)

Error Handling

The LPC package provides specific exception types for different error scenarios:

from dcnr.lpc import exec, register_proc
from dcnr.lpc import ProcedureNotFoundError, ProcedureExecutionError, LPCError

# Handle procedure not found
try:
    result = exec("nonexistent.procedure", 1, 2, 3)
except ProcedureNotFoundError as e:
    print(f"Procedure not found: {e}")

# Handle execution errors
def risky_procedure(x):
    if x < 0:
        raise ValueError("Negative values not allowed")
    return x * 2

register_proc("math.risky", risky_procedure)

try:
    result = exec("math.risky", -5)
except ProcedureExecutionError as e:
    print(f"Procedure execution failed: {e}")
    # Original exception available via e.__cause__

# Handle registration errors
try:
    register_proc("invalid.proc", "not_a_function")
except TypeError as e:
    print(f"Registration failed: {e}")

# Catch all LPC errors
try:
    result = exec("some.procedure", arg1, arg2)
except LPCError as e:
    print(f"LPC error occurred: {e}")

Best Practices

Naming Conventions

Use hierarchical naming for better organization:

# Good naming patterns
register_proc("auth.login", login_user)
register_proc("auth.logout", logout_user)
register_proc("data.users.create", create_user)
register_proc("data.users.update", update_user)
register_proc("utils.validation.email", validate_email)
register_proc("utils.formatting.currency", format_currency)

Error-Safe Execution

from dcnr.lpc import exec, has_proc, ProcedureNotFoundError

def safe_procedure_call(proc_name, *args, default=None, **kwargs):
    """Execute procedure with fallback to default value."""
    try:
        if has_proc(proc_name):
            return exec(proc_name, *args, **kwargs)
        else:
            print(f"Warning: Procedure '{proc_name}' not registered")
            return default
    except Exception as e:
        print(f"Error executing '{proc_name}': {e}")
        return default

# Usage
result = safe_procedure_call("math.divide", 10, 2, default=0)

Procedure Documentation

from dcnr.lpc import register_proc, list_procs

def documented_procedure(x, y):
    """
    Add two numbers together.
    
    Args:
        x: First number
        y: Second number
    
    Returns:
        Sum of x and y
    """
    return x + y

register_proc("math.add", documented_procedure)

# Access documentation
from dcnr.lpc import get_proc
proc = get_proc("math.add")
print(proc.__doc__)  # Prints the docstring

Error Handling

Sandbox Errors

The sandbox raises specific exceptions for different types of violations:

from dcnr.minisandbox import sandbox_exec, SandboxSyntaxError, SandboxRuntimeError

try:
    # This will raise SandboxSyntaxError
    sandbox_exec("import os", {})
except SandboxSyntaxError as e:
    print(f"Syntax violation: {e}")

try:
    # This will raise SandboxRuntimeError  
    sandbox_exec("unknown_variable", {})
except SandboxRuntimeError as e:
    print(f"Runtime error: {e}")

Registry Errors

The registry raises standard Python exceptions for invalid operations:

from dcnr.minisandbox import register_object, get_registry

try:
    # Invalid path component
    register_object("app.for.test", 1)  # 'for' is a Python keyword
except ValueError as e:
    print(f"Invalid path: {e}")

try:
    # Path not found
    registry = get_registry()
    value = registry.get_object("nonexistent.path")
except KeyError as e:
    print(f"Path not found: {e}")

Safety Features

Iteration Limits

Loops are automatically limited to 100,000 iterations to prevent infinite loops:

# This will raise SandboxRuntimeError after 100,000 iterations
code = """
i = 0
while True:
    i += 1
"""

Memory Safety

Only safe data types and operations are allowed. No access to system resources or dangerous built-ins.

Use Cases

Sandbox Execution

  • Educational platforms - Safe execution of student code
  • Code challenges - Running untrusted submissions
  • Configuration scripts - Controlled execution of user-defined logic
  • Expression evaluation - Safe calculation of mathematical expressions
  • Templating - Dynamic value computation in templates

Object Registry

  • Application configuration - Centralized storage of settings and parameters
  • Service registration - Registry for application services and dependencies
  • Plugin systems - Dynamic registration and discovery of plugins
  • Feature flags - Hierarchical feature toggle management
  • Resource management - Centralized access to database connections, APIs, etc.

LPC (Local Procedure Call)

  • Plugin architectures - Dynamic loading and execution of plugin methods
  • Microservices - Anonymous procedure calls between service components
  • Workflow engines - Step-by-step execution of named procedures in workflows
  • API routing - Map request paths to procedure implementations
  • Command pattern - Decouple command invocation from implementation
  • Data processing pipelines - Chain procedures for data transformation
  • Event handling - Register and execute event handlers by name
  • Configuration-driven execution - Execute procedures based on configuration files

Limitations

  • No custom function definitions
  • No module imports or external libraries
  • Limited to basic Python constructs
  • No file system or network access
  • Fixed iteration limits for loops

Development

To contribute to dcnr-minisandbox:

  1. Clone the repository
  2. Install development dependencies
  3. Run tests with your changes
  4. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Author

Peter Kollath (peter.kollath@gopal.home.sk)

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dcnr_minisandbox-1.0.7.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dcnr_minisandbox-1.0.7-py3-none-any.whl (30.2 kB view details)

Uploaded Python 3

File details

Details for the file dcnr_minisandbox-1.0.7.tar.gz.

File metadata

  • Download URL: dcnr_minisandbox-1.0.7.tar.gz
  • Upload date:
  • Size: 33.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.4

File hashes

Hashes for dcnr_minisandbox-1.0.7.tar.gz
Algorithm Hash digest
SHA256 cd9e14b9bd8fe6a7ae403bb21856638f1e23c5bc50d1a66b8b43f0710504b91b
MD5 d7d17b8d51366826be0fd83436829ad7
BLAKE2b-256 9cbcd32fa120fdecc3864a14de8d8ae992913ca427a1d9351f81f263e62aa9a6

See more details on using hashes here.

File details

Details for the file dcnr_minisandbox-1.0.7-py3-none-any.whl.

File metadata

File hashes

Hashes for dcnr_minisandbox-1.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 88674f8c15b7e112158960733f5223a51081bc6b7ad618c3084480c95791730a
MD5 1d618871b4de54bc03370a202a4bc555
BLAKE2b-256 561de4fc782e3a6cb3f699292f723eb4e68c8a59defd3dc85a9a89a7af7d6cf5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page