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 (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

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

The registry provides a hierarchical namespace 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

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.3.tar.gz (19.4 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.3-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dcnr_minisandbox-1.0.3.tar.gz
  • Upload date:
  • Size: 19.4 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.3.tar.gz
Algorithm Hash digest
SHA256 676f02129f15d4e6e8e5ba30735c8553946e29fa2d0c7a1ad79a61c7dea9da4a
MD5 481982c392164eb09ec4f3c2cca7da1e
BLAKE2b-256 addb94266f74b5d0282aa11587807e99f81987adf0d2c91eb1009e82c5587ef7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for dcnr_minisandbox-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 da3ce916c2d2655df62fba58a13e694ebd528f6591c80711bf1f749a33797a35
MD5 6dc502b6e82614ec95aadc8a3b487da2
BLAKE2b-256 c5659c1bf3d17351ce6fb44745c728b1e44df5ae77b374b0dcad8c7290661056

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