ast.parse() 是安全解析未信任代码的唯一入口,仅生成ast不执行;需配合自定义nodevisitor拦截import、call、attribute等危险节点,并严格检查上下文(如load/store)和链式访问,禁用compile()/exec(),不可依赖literal_eval()。

怎么用 ast.parse() 安全地解析未信任代码
直接 eval() 或 exec() 用户输入是高危操作,ast.parse() 是唯一安全的替代入口。它不执行,只生成语法树,但默认仍会解析所有合法 Python 语法——包括 ast.Import、ast.Call 等可能触发副作用的节点(比如 __import__('os').system('rm -rf /') 在 parse 阶段不会执行,但后续遍历时若不加限制,就可能被误放行)。
实操建议:
- 始终传入
mode='exec'(默认值),避免因mode='eval'导致解析多语句时报SyntaxError: invalid syntax - 捕获
SyntaxError并返回具体错误位置:except SyntaxError as e: print(f"Line {e.lineno}, col {e.offset}: {e.msg}") - 不要对原始 AST 节点调用
compile()或exec()—— 这等于绕过审查,前功尽弃
如何识别并拦截危险 AST 节点类型
静态审查的核心不是“允许什么”,而是“禁止什么”。Python AST 中真正需要拦截的节点极少,但漏掉任意一个都可能导致 RCE 或信息泄露。
常见错误现象:只检查 ast.Call,却忽略 ast.Attribute(如 os.system 的左值)或 ast.Subscript(如 __builtins__['open']);或者用字符串匹配函数名,结果被 getattr(__import__('os'), 'system') 绕过。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 必须拦截的节点类型:
ast.Import、ast.ImportFrom、ast.Call、ast.Attribute、ast.Subscript、ast.Name(当ctx是ast.Load且名字在危险列表中) - 危险标识符列表至少包含:
__import__、eval、exec、compile、open、os、sys、subprocess、builtins - 对
ast.Attribute要递归检查链式访问:a.b.c需确认a是否为危险名,不能只看c
ast.NodeVisitor 遍历时容易忽略的上下文陷阱
很多工具用 ast.NodeVisitor 实现白名单逻辑,但没意识到节点的 ctx 属性(如 ast.Load / ast.Store)和父节点类型共同决定语义。比如 os = 1 中的 os 是 ast.Store,不该报错;但 os.system() 中的 os 是 ast.Load,必须拦截。
性能影响:深度递归遍历大型 AST 可能触发 Python 默认递归限制(RecursionError),尤其嵌套字典/列表推导式较多时。
实操建议:
- 重写
visit_Name(self, node)时,先判断isinstance(node.ctx, ast.Load)再查黑名单 - 用
sys.setrecursionlimit(3000)临时放宽限制(仅限审查阶段,非生产执行) - 避免在
visit_*方法中做耗时操作(如正则匹配、网络请求),审查应纯内存计算
为什么不能依赖 ast.literal_eval() 做通用审查
ast.literal_eval() 确实安全,但它只支持极小的子集:数字、字符串、元组、列表、字典、布尔值、None。一旦用户输入含函数调用、变量引用、运算符(如 1 + 1)、甚至注释,就会抛出 ValueError: malformed node or string —— 这不是审查失败,是能力缺失。
使用场景错配典型表现:把配置文件校验当成代码审查,或误以为 “只允许字面量” 就等于 “足够安全”。
实操建议:
- 如果业务只要求解析静态数据,用
ast.literal_eval()没问题;但凡涉及变量、调用、导入,就必须切回ast.parse()+ 自定义遍历 - 不要试图给
ast.literal_eval()打补丁(比如预处理替换变量名)—— 语义已丢失,补丁越写越不可靠 - 注意它不支持 f-string、海象运算符(
:=)、类型注解等 Python 3.6+ 特性,版本兼容性需手动对齐
真正难的不是写遍历逻辑,是厘清「哪些节点组合起来才构成可执行风险」。比如单独一个 ast.Name(id='os') 不危险,但它是 ast.Attribute(value=..., attr='system') 的 value 时,整条链就该熔断。这种上下文敏感性,没法靠单节点匹配解决。










