自定义数学函数输入场景下替代eval()的安全方案评估与优化问询
I want to let users input custom math functions to customize my program's workflow. At first, I used eval() like this:
import numpy as np x = linspace(0, np.pi) y = eval(input('y(x)=')) # Process the custom y(x)...
But obviously, eval() has serious security risks—for example, a malicious user could input something like eval("__import__('os').system('clear')", {}) to run arbitrary commands, which is not what I want.
So I designed this processing flow after some research:
import numpy as np x = np.linspace(0, 2*np.pi) y = None user_input = str(input('y(x)=')) # Filter dangerous strings forbidden_strings = ['import', '\n', '.py', '.exe', '.npz', '.npy', 'load', 'exec', 'compile', ';'] for forbidden_string in forbidden_strings: if forbidden_string in user_input: raise ValueError("FORBIDDEN COMMAND IN INPUT !") # Set allowed numpy functions list allowed_functions = 'mean, max, min, sum, pi, cos, sin, tan, arccos, arcsin, arctan, log, exp, sqrt, power, gradient' code = "from numpy import " + allowed_functions +"\ny=" + user_input # The generated code looks like: # from numpy import all allowed functions # y= user input exec(compile(code, filename="", mode="exec"))
Is this solution secure enough? Are there any ways to further enhance its security? Thanks for your help!
Great question—your current approach is a solid step up from raw eval(), but it still has critical gaps that malicious users could exploit. Let’s break this down clearly:
Why your current solution isn’t fully secure
- Fragile string filtering: Attackers can easily bypass your forbidden list with tricks like uppercase letters (
IMPORT), Unicode homoglyphs, or string concatenation ('imp'+'ort'). Your filter won’t catch any of these workarounds. - Hidden runtime access: Even with your allowed imports, users can dig into Python’s internal structures. For example, inputting
cos.__class__.__base__.__subclasses__()lets them enumerate and access dangerous built-in classes that could lead to arbitrary code execution. - Resource exhaustion risks: Users could input expressions that crash or hang your program, like
power(10, 10000000)which creates an enormous number, or nested recursive calls that eat up memory.
Actionable ways to harden security
Here are practical, robust improvements you can implement:
- Use AST validation instead of string filtering
Python’s built-inastmodule lets you parse the user’s input into an abstract syntax tree, then validate every single node to ensure it only uses safe, allowed constructs. This is infinitely more reliable than blacklisting strings.
Example implementation:
import ast import numpy as np x = np.linspace(0, 2*np.pi) # Define allowed AST node types (math operations, constants, function calls) allowed_nodes = { ast.Name, ast.Constant, ast.BinOp, ast.UnaryOp, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.Neg, ast.Pos, ast.Call } # Whitelist of safe numpy functions/values allowed_funcs = {'cos', 'sin', 'tan', 'pi', 'exp', 'sqrt', 'log', 'mean', 'max'} def validate_expression(node): if type(node) not in allowed_nodes: raise ValueError(f"Invalid expression part: {type(node).__name__}") # Check function calls are only to allowed functions if isinstance(node, ast.Call): if not isinstance(node.func, ast.Name) or node.func.id not in allowed_funcs: raise ValueError(f"Function '{node.func.id}' is not allowed") # Validate all arguments to the function for arg in node.args: validate_expression(arg) # Recursively validate child nodes for child in ast.iter_child_nodes(node): validate_expression(child) user_input = input('y(x)=') try: # Parse input as an evaluatable expression tree = ast.parse(user_input, mode='eval') validate_expression(tree.body) # Execute with a strictly restricted scope safe_globals = {k: getattr(np, k) for k in allowed_funcs} safe_globals['x'] = x y = eval(compile(tree, filename='', mode='eval'), safe_globals, {}) except (SyntaxError, ValueError) as e: print(f"Invalid input: {e}")
This ensures only explicitly allowed operations and functions are used—no bypasses possible.
Lock down the execution scope
Never use the default global scope withexecoreval(which includes full access to__builtins__). Instead, pass a minimal dictionary containing only the allowed functions and variables (like thesafe_globalsin the example above). This eliminates access to dangerous built-ins likeopenor__import__.Ditch blacklists entirely
Blacklist-based filtering is a losing battle—there are endless ways to work around them. Always use a whitelist approach (like AST validation) where you only permit known-safe constructs.Add resource limits
Even safe-looking math can crash your program. Use tools like theresourcemodule (on Unix) orwin32api(on Windows) to set CPU time and memory limits for evaluating the input. Alternatively, run the evaluation in a separate process that you can terminate if it hangs or uses too many resources.Test edge cases aggressively
Try inputting these test cases to see if your implementation catches them:
cos.__globals__['__builtins__']['eval']("__import__('os').system('echo hacked')")(lambda x: __import__('os'))()10**100000000
If your current code doesn’t block these, it’s a clear sign you need to tighten security.
Final takeaway
Your current approach is better than raw eval(), but it’s not fully secure. The most reliable method is combining AST validation with strict scope restriction—this eliminates nearly all attack surfaces and gives you full control over what users can do.
内容的提问来源于stack exchange,提问作者John

