Python获取嵌套属性时eval()的替代方案、合理使用场景及安全优化问题
这确实是Python中处理动态属性路径时很常见的痛点——eval虽然好用,但安全风险始终是悬在头上的达摩克利斯之剑。咱们一步步来拆解你的问题:
1. 该场景是否属于eval()的合理使用场景?
答案是:仅当你的配置文件完全来自可信、可控的源时,才算是“相对合理”的场景。如果配置可能被外部用户修改、或者来自不可信的输入渠道,那绝对不应该用eval——哪怕你做了前缀过滤,攻击者依然能构造出绕过限制的恶意代码(比如利用属性链访问内置对象,比如foo.__class__.__base__.__subclasses__()这类操作,能拿到危险的内置类)。
如果你的配置是内部维护、不会被篡改的静态文件,那短期用eval凑活没问题,但从长期维护和安全规范来看,还是建议换成更安全的方案。
2. 你遗漏的替代方案
这里有几种复杂度适中、安全性更高的方案,适合你的场景:
方案一:自定义表达式解析器
手动拆分配置中的路径表达式,逐个处理属性访问、字典/列表索引,甚至有限的方法调用。这种方案完全可控,不会有eval的安全风险。
举个简单的实现例子:
import re def resolve_expression(obj, expr): # 拆分表达式,处理.、[]、()这类分隔符 tokens = re.split(r'(\.|\[|\]|\()', expr) tokens = [t.strip() for t in tokens if t.strip() not in ('', '.', '[', ']', '(')] current = obj for token in tokens: if token.endswith(')'): # 处理无参数方法调用(比如toList()) method_name = token[:-2] # 白名单限制允许调用的方法 allowed_methods = {'toList'} if method_name not in allowed_methods: raise ValueError(f"Method {method_name} is not permitted") if not hasattr(current, method_name): raise AttributeError(f"Object has no method {method_name}") current = getattr(current, method_name)() elif token.startswith(("'", '"')): # 处理字典字符串键 key = token.strip("'\"") current = current[key] elif token.isdigit(): # 处理列表索引 current = current[int(token)] else: # 处理类属性访问 current = getattr(current, token) return current # 测试示例 class Bar: def __init__(self): self.attr_x = 10 class Foo: def __init__(self): self.class_bar = Bar() self.dict_x = {'xy': [1,2,3]} self.list_x = [4,5,6] def toList(self): return list(self.dict_x.values())[0] foo = Foo() print(resolve_expression(foo, "class_bar.attr_x")) # 输出10 print(resolve_expression(foo, "dict_x['xy']")) # 输出[1,2,3] print(resolve_expression(foo, "list_x[0]")) # 输出4
这个解析器可以根据你的需求扩展,比如支持带参数的方法(但要严格限制参数类型),或者更复杂的索引规则。
方案二:使用AST模块做安全检查
利用Python的ast模块解析表达式,遍历抽象语法树,只允许安全的节点类型,然后再执行。这种方式比eval安全得多,因为你可以精确控制哪些操作是允许的。
示例代码:
import ast def safe_eval(obj, expr): # 将表达式解析为抽象语法树 tree = ast.parse(expr, mode='eval') # 定义允许的节点类型 allowed_nodes = ( ast.Attribute, ast.Name, ast.Subscript, ast.Constant, ast.Call, ast.Index ) # 遍历AST,检查所有节点是否合法 for node in ast.walk(tree): if not isinstance(node, allowed_nodes): raise ValueError(f"Invalid expression: {type(node).__name__} is not allowed") # 额外限制方法调用:只允许指定方法,且无参数 if isinstance(node, ast.Call): if not isinstance(node.func, ast.Attribute): raise ValueError("Only method calls on objects are allowed") method_name = node.func.attr allowed_methods = {'toList'} if method_name not in allowed_methods: raise ValueError(f"Method {method_name} is not permitted") if len(node.args) > 0 or node.keywords: raise ValueError("Method calls with arguments are not allowed") # 执行安全表达式,仅传入必要的变量 return eval(compile(tree, filename='', mode='eval'), {'foo': obj}, {}) # 测试示例 foo = Foo() print(safe_eval(foo, "foo.class_bar.attr_x")) # 输出10 print(safe_eval(foo, "foo.dict_x['xy']")) # 输出[1,2,3]
这个方案能处理更复杂的合法表达式,同时过滤掉所有危险操作(比如赋值、导入、调用危险内置函数)。
方案三:使用专用的路径查询库
如果你的表达式格式比较规范,可以考虑用objectpath或者jsonpath-ng这类库,它们专门用来查询嵌套对象的属性/元素,不需要用eval。比如objectpath支持类似$.class_bar.attr_x的语法,你只需要把配置中的foo.替换成$.即可。
3. 如何更安全地使用eval()?
如果因为某些原因你必须继续使用eval,那一定要做以下几层防护:
严格限制命名空间:不要让eval访问全局的内置函数,只传入必要的变量。比如:
def get_attr(foo, statement): # 仅传入foo,禁用所有内置函数 return eval(f"foo.{statement}", {"__builtins__": None}, {"foo": foo})这样攻击者无法调用
__import__、exec等危险函数。AST语法检查:在执行eval之前,先用ast模块解析表达式,过滤掉所有危险节点(比如Assign、Import、Lambda等),就像方案二里的做法。
表达式白名单过滤:提前用正则表达式做初步过滤,只允许包含合法的字符和结构:
import re def is_safe_expression(statement): pattern = r'^[a-zA-Z0-9_]+\.([a-zA-Z0-9_]+|\[[\'"]?[a-zA-Z0-9_]+[\'"]?\]|\([^\)]*\))(\.([a-zA-Z0-9_]+|\[[\'"]?[a-zA-Z0-9_]+[\'"]?\]|\([^\)]*\)))*$' return re.match(pattern, statement) is not None注意,正则只是初步过滤,不能完全替代AST检查,但能挡住大部分明显的恶意语句。
内容的提问来源于stack exchange,提问作者JustFloooo

