如何在Python绘图程序中安全处理用户自定义数学函数输入
嘿,这个问题我之前做类似的数学绘图工具时也踩过坑!用eval()确实太危险了——随便输入个__import__('os').system('rm -rf /')就能搞事情,完全不能给用户开放这个权限。ast.literal_eval()虽然安全,但确实只能处理字面量,连函数调用都不让过,根本满足不了数学公式的需求。
下面给你几个靠谱的解决方案,从灵活到省心排序:
ast模块解析语法树(最灵活可控) 这个方法是把用户输入的公式解析成抽象语法树(AST),然后遍历检查每个节点,只允许我们预先指定的操作、函数和变量,其他任何内容直接拒绝。比如只允许加减乘除幂运算,sin/cos/log这些常用数学函数,变量只能是x。
直接上代码示例:
import ast import math # 定义允许使用的数学函数 ALLOWED_FUNCTIONS = { 'sin': math.sin, 'cos': math.cos, 'tan': math.tan, 'log': math.log, 'exp': math.exp, 'sqrt': math.sqrt } # 定义允许使用的运算符 ALLOWED_OPERATORS = { ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.USub, ast.UAdd # 支持正负号,比如 -x 或者 +x } def safe_eval_math(expr, x_value): try: # 把表达式解析成AST tree = ast.parse(expr, mode='eval') except SyntaxError: raise ValueError("表达式语法错误,请检查输入") # 递归遍历AST节点并求值 def _traverse(node): if isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.Name): # 只允许变量x if node.id == 'x': return x_value else: raise ValueError(f"不允许使用未定义的变量:{node.id}") elif isinstance(node, ast.Call): # 检查函数是否在允许列表里 if isinstance(node.func, ast.Name) and node.func.id in ALLOWED_FUNCTIONS: # 递归处理函数参数 args = [_traverse(arg) for arg in node.args] return ALLOWED_FUNCTIONS[node.func.id](*args) else: raise ValueError(f"不允许使用的函数:{node.func.id}") elif isinstance(node, ast.BinOp): # 检查运算符是否允许 if type(node.op) in ALLOWED_OPERATORS: left_val = _traverse(node.left) right_val = _traverse(node.right) # 额外做一些安全限制,比如防止除零、超大指数 if isinstance(node.op, ast.Div): if right_val == 0: raise ValueError("不能除以零") return left_val / right_val elif isinstance(node.op, ast.Pow): if abs(right_val) > 100: raise ValueError("指数过大,超出计算范围") return left_val ** right_val # 其他运算符直接计算 return type(node.op)(left_val, right_val) else: raise ValueError(f"不允许使用的运算符:{type(node.op).__name__}") elif isinstance(node, ast.UnaryOp): if type(node.op) in ALLOWED_OPERATORS: operand_val = _traverse(node.operand) return type(node.op)(operand_val) else: raise ValueError(f"不允许使用的一元运算符:{type(node.op).__name__}") else: # 遇到未知的语法节点直接拒绝 raise ValueError(f"不允许的语法内容:{type(node).__name__}") return _traverse(tree.body)
测试一下:输入2*x*sin(x)完全没问题,但输入__import__('os').system('echo hacked')会直接报错,因为__import__不在允许的函数列表里。
如果不想自己写AST解析逻辑,直接用现成的数学库就行,比如sympy或者numexpr——这些库本来就是为处理数学表达式设计的,自带安全机制,不会执行任意Python代码。
以sympy为例:
import sympy from sympy import symbols, sympify # 定义变量x x = symbols('x') def safe_eval_sympy(expr): try: # 把字符串转换成SymPy表达式对象 func_expr = sympify(expr) # 检查表达式里的变量只有x if set(func_expr.free_symbols) != {x}: raise ValueError("只允许使用变量x") # 限制允许使用的函数 allowed_funcs = {'sin', 'cos', 'tan', 'log', 'exp', 'sqrt'} for func in func_expr.atoms(sympy.Function): if func.__class__.__name__ not in allowed_funcs: raise ValueError(f"不允许使用的函数:{func.__class__.__name__}") # 返回一个可以直接传入x值计算的函数 return lambda x_val: float(func_expr.subs(x, x_val)) except Exception as e: raise ValueError(f"无效表达式:{str(e)}")
sympify会自动过滤掉危险的Python代码,而且还能帮你处理表达式的化简,非常适合做数学绘图的场景。
如果对安全性要求极高,比如要开放给公共用户使用,可以用RestrictedPython这种沙箱库,它会严格限制代码能访问的模块、函数和属性,只开放安全的部分。
示例代码:
import math from RestrictedPython import compile_restricted from RestrictedPython.Eval import default_guarded_getitem # 构建安全的全局环境,只开放允许的内容 safe_globals = { 'math': math, 'x': None, # 后续动态传入x的值 '__builtins__': { 'abs': abs, 'pow': pow, }, '_getitem_': default_guarded_getitem, # 允许列表/字典的索引操作(如果需要的话) } def safe_eval_sandbox(expr, x_value): safe_globals['x'] = x_value try: # 编译受限代码 byte_code = compile_restricted(expr, '<user_input>', 'eval') # 在安全环境中执行 result = eval(byte_code, safe_globals) return result except Exception as e: raise ValueError(f"无效或不安全的表达式:{str(e)}")
这种方式能彻底隔离恶意代码,但配置起来需要注意细节,比如哪些内置函数可以开放,哪些不能。
个人推荐第一种AST解析的方法——灵活性最高,完全可控,能根据你的绘图需求定制允许的操作;如果追求开发效率,用
sympy会更省心,不用自己写解析逻辑。绝对不要用eval()直接处理用户输入,风险真的太高了!
内容的提问来源于stack exchange,提问作者WB-man

