Skip to content

Replace bare eval() with AST-whitelist sandbox for safe expression evaluation#45

Open
yunsmall wants to merge 1 commit into
78:mainfrom
yunsmall:main
Open

Replace bare eval() with AST-whitelist sandbox for safe expression evaluation#45
yunsmall wants to merge 1 commit into
78:mainfrom
yunsmall:main

Conversation

@yunsmall

@yunsmall yunsmall commented Jun 12, 2026

Copy link
Copy Markdown

Summary

This PR replaces the unsafe eval() call in calculator.py with an AST-whitelist-based sandbox evaluator, preventing sandbox escape attacks while preserving full mathematical functionality.

Problem

The original code used:

eval(python_expression, {"math": math, "random": random})

This is vulnerable to Python sandbox escape. Because __builtins__ is auto-injected by CPython, an attacker can bypass the restricted globals via:

().__class__.__bases__[0].__subclasses__()
# → access os.system, subprocess, etc.

Solution

Architecture

File Purpose
whitelist.py (277 lines) Data-only: AST node types, function whitelists, safe execution environment
safe_eval.py (173 lines) Logic-only: recursive AST validation + safe compile()+ eval()
calculator.py (44 lines) Thin MCP server, imports from the above two

Security model

  1. AST whitelist — only explicitly allowed node types pass (not a blacklist)
  2. __builtins__ set to empty dict — no builtins leak
  3. Per-node recursive validation — Attribute, Call, and Name nodes individually checked
  4. Submodule path validation — nested attributes like numpy.fft.fft are allowed only if every intermediate segment is a registered submodule with its own whitelist
  5. Errors returned to AI — blocked expressions return {success: False, error: "..."} so the AI can self-correct instead of crashing

What's supported

  • math: full module (all functions + constants)
  • random: full module
  • numpy (optional): 200+ functions including:
    • All common array ops, linear algebra (numpy.linalg.*), FFT (numpy.fft.*), random distributions (numpy.random.*)
    • Nested submodule access (numpy.fft.fft, numpy.linalg.det, etc.)
    • np alias fully supported
  • Safe builtins: abs, min, max, sum, range, len, sorted, zip, map, etc.
  • Comprehensions, ternary expressions, bitwise ops, all standard operators
  • @ matrix multiply operator
  • Keyword arguments (axis=, dtype=, etc.)

What's blocked

  • All dunder attribute access (__class__, __bases__, __subclasses__, __globals__, etc.)
  • Dangerous builtins (open, eval, exec, __import__, compile, globals, getattr, etc.)
  • File I/O (numpy.load, numpy.save, numpy.fromfile, etc.)
  • Walrus operator, f-strings, lambda, method calls on literals
  • Unicode homoglyph bypass attempts

Testing

test_sandbox.py456 tests, all passing:

  • Functional tests for math, random, numpy (top-level + all 3 submodules), builtins, comprehensions, edge cases
  • Sandbox escape tests: dunder chains, dangerous builtins, module injection, expression-based exploits, tricky edge cases (walrus, f-string, format injection, short-circuit bypass, unicode homoglyphs, dict unpack injection, comprehension dunder)

@yunsmall yunsmall force-pushed the main branch 12 times, most recently from 573985b to e59d98a Compare June 13, 2026 10:02
…aluation

- Add safe_eval.py: AST node-by-node whitelist validation
- Add whitelist.py: data-only module with allowed nodes/functions/modules
- Rewrite calculator.py: use safe_eval, return errors to AI instead of crashing
- Add test_sandbox.py: 390 tests covering functionality and sandbox escape attempts
- Support math, random, and numpy (optional) with submodule nesting (numpy.fft.fft, etc.)
- Block all dunder attribute escapes, dangerous builtins, and file I/O
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant