Python ast 模块的代码审查工具

2次阅读

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

Python ast 模块的代码审查工具

怎么用 ast.parse() 安全地解析未信任代码

直接 eval()exec() 用户输入是高危操作,ast.parse() 是唯一安全的替代入口。它不执行,只生成语法树,但默认仍会解析所有合法 python 语法——包括 ast.Importast.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.Importast.ImportFromast.Callast.Attributeast.Subscriptast.Name(当 ctxast.Load 且名字在危险列表中)
  • 危险标识符列表至少包含:__import__evalexeccompileopenossyssubprocessbuiltins
  • ast.Attribute递归检查链式访问:a.b.c 需确认 a 是否为危险名,不能只看 c

ast.NodeVisitor 遍历时容易忽略的上下文陷阱

很多工具ast.NodeVisitor 实现白名单逻辑,但没意识到节点的 ctx 属性(如 ast.Load / ast.Store)和父节点类型共同决定语义。比如 os = 1 中的 osast.Store,不该报错;但 os.system() 中的 osast.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 时,整条链就该熔断。这种上下文敏感性,没法靠单节点匹配解决。

text=ZqhQzanResources