Variable+ evaluates mathematical expressions using Python's eval() function. To prevent code injection and malicious operations, it implements a multi-layer security model.
| Threat | Protection | Layer |
|---|---|---|
| Arbitrary code execution | AST validation blocks imports, function/class defs | Evaluator |
| System access | __builtins__ removed from namespace |
Evaluator |
Sandbox escape via __class__ |
Forbidden attribute access list | Evaluator |
| Resource exhaustion (DoS) | No range(), list(), enumerate() |
Evaluator |
| Data exfiltration | No file I/O, network, or system calls | Evaluator |
| Infinite loops | No while/for/function definitions |
Evaluator |
| Threat | Reason |
|---|---|
| CPU-intensive expressions | calc("9**9**9**9") will consume CPU (Python limitation) |
| Memory via large numbers | calc("10**100000000") can exhaust memory |
| Floating-point errors | Division by near-zero values may hang |
| Timing side-channels | Expression evaluation time varies with complexity |
The tokenizer only accepts characters from the defined symbol table (table.json). Unknown characters raise UnknownSymbolError before any evaluation occurs.
This means arbitrary Python syntax like __import__, exec, eval cannot even be tokenized through the standard calc() entry point.
After translation to Python, the expression is parsed into an AST (Abstract Syntax Tree) and validated by SafeNodeVisitor. This blocks:
| Node Type | What It Blocks |
|---|---|
ast.Import / ast.ImportFrom |
import os, from os import system |
ast.FunctionDef / ast.AsyncFunctionDef |
def f(): ... |
ast.ClassDef |
class X: ... |
ast.Lambda |
lambda x: x |
ast.Attribute (filtered) |
x.__class__, x.__builtins__ |
ast.Call (filtered) |
Only allows whitelisted function calls |
Only these function calls pass AST validation:
| Category | Functions |
|---|---|
math.* |
Any function from the math module |
| Built-in safe | abs, round, min, max, sum, pow |
| Trig reciprocals | sec, csc, cot |
| Other | sign, cbrt, fourthrt |
Any other function call raises SecurityError.
Access to these attribute names is always blocked:
__builtins__ __class__ __subclasses__
__bases__ __mro__ __globals__
__code__ __import__ __getattr__
__setattr__ __delattr__ __dict__
The expression is evaluated with a namespace that has __builtins__ set to an empty dictionary. This means:
- No
print(),input(),open(),exec(),eval() - No
dir(),vars(),globals(),locals() - No
type(),isinstance(),getattr(),setattr() - No
compile(),__import__()
Only the explicitly whitelisted names are available:
ALLOWED_NAMES = {
"math": math, # Full math module
"abs": abs, # Absolute value
"round": round, # Rounding
"min": min, # Minimum
"max": max, # Maximum
"sum": sum, # Summation
"pow": pow, # Power
"sec": _sec, # Secant
"csc": _csc, # Cosecant
"cot": _cot, # Cotangent
"sign": _sign, # Sign function
"cbrt": _cbrt, # Cube root
"fourthrt": _fourthrt, # Fourth root
"e": math.e, # Euler's number
"j": 1j, # Imaginary unit
"i": 1j, # Imaginary unit
"True": True, # Boolean
"False": False, # Boolean
}Plus user-provided variables.
calc("__import__('os').system('rm -rf /')")
# Blocked at tokenizer: __import__ is not a valid token
# If somehow bypassed: SafeNodeVisitor blocks ast.Import
calc("import os")
# SecurityError: Import statements are not allowedcalc("().__class__.__bases__[0].__subclasses__()")
# SecurityError: Access to '__class__' is not allowedcalc("exec('import os')")
# SecurityError: Function call 'exec' is not allowed
# (exec is not in the whitelist)
calc("eval('1+1')")
# SecurityError: Function call 'eval' is not allowed# Previously possible (now blocked):
calc("list(range(999999999))")
# SecurityError: Function call 'list' is not allowed
# (list and range removed from ALLOWED_NAMES in v0.5)- Validate input length — Limit expression string length to prevent very long expressions.
- Set timeouts — Wrap
calc()in a timeout for untrusted input. - Don't pass user data as variables blindly — Variable names from user input should be validated.
- Don't use
evaluate()directly — Always go throughcalc()for full pipeline security.
- Never add
eval,exec,compileto ALLOWED_NAMES. - Never add
getattr,setattr,typeto ALLOWED_NAMES. - Never add
list,dict,tuplewithrange— enables DoS. - Test new functions against the SafeNodeVisitor whitelist.
- Review
table.jsonchanges for lambda injections or arbitrary code.
import signal
class TimeoutError(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutError("Calculation timed out")
def safe_calc(expression, max_length=500, timeout_seconds=5, **variables):
"""Calculate with safety limits for untrusted input."""
# Limit expression length
if len(expression) > max_length:
raise ValueError(f"Expression too long: {len(expression)} > {max_length}")
# Set timeout (Unix only)
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)
try:
return calc(expression, **variables)
finally:
signal.alarm(0) # Cancel timeout| Approach | Security | Functionality | Variable+ |
|---|---|---|---|
Raw eval() |
None | Full Python | N/A |
ast.literal_eval() |
High | Literals only | N/A |
eval() with __builtins__={} |
Medium | Easy to bypass | Layer 3 only |
| Variable+ | High | Math expressions | All 3 layers |
SymPy .evalf() |
High | Symbolic math | Different scope |