
本文介绍如何在不使用危险的 eval 或 exec 的前提下,对用户提交的 Python 源码字符串进行安全的运行时追踪(tracing),通过 AST 解析、动态编译与 sys.settrace 配合实现可控的变量赋值与调用行为监控。
本文介绍如何在不使用危险的 `eval` 或 `exec` 的前提下,对用户提交的 python 源码字符串进行安全的运行时追踪(tracing),通过 ast 解析、动态编译与 `sys.settrace` 配合实现可控的变量赋值与调用行为监控。
在 Python 动态分析与调试工具开发中,常需对任意源码片段(尤其是用户提交的不可信代码)进行执行期行为追踪,例如记录变量赋值、函数调用、行号跳转等。但直接使用 eval() 或 exec() 执行字符串代码存在严重安全风险——它们可执行任意系统命令、访问全局命名空间、甚至绕过沙箱限制。
✅ 正确路径是:将源码字符串安全地编译为代码对象(code object),再在受控作用域中执行,并配合 sys.settrace 实现细粒度追踪。
1. 安全编译:从字符串到 code 对象
使用 compile() 函数(而非 exec)将源码编译为可执行的 code 对象,指定 mode='exec'(适用于多语句,如含函数定义)或 'eval'(单表达式):
import ast
import sys
import functools
source_code = """
def adder(x, y):
return x + y
result = adder(3, 5)
print('Done')
"""
# 安全编译:不执行,仅生成 code object
code_obj = compile(source_code, filename="<user_input>", mode="exec")</user_input>
⚠️ 注意:compile() 本身不执行代码,无注入风险;它只做语法检查和字节码生成。
2. 受控执行环境 + 追踪启用
创建干净的局部/全局命名空间,并在执行前后精准启用/禁用 sys.settrace:
def trace_calls(frame, event, arg):
if event == "line":
print(f"[LINE] {frame.f_lineno}: {frame.f_code.co_name}")
elif event == "call":
print(f"[CALL] {frame.f_code.co_name}()")
elif event == "return":
print(f"[RETURN] {frame.f_code.co_name} → {arg}")
return trace_calls # 继续追踪子调用
def safe_trace_exec(code_obj, globals_dict=None, locals_dict=None):
# 创建最小化、隔离的执行环境
safe_globals = {
"__builtins__": {}, # 禁用所有内置函数(可按需白名单添加)
"print": lambda *a: print("[TRACED]", *a),
"len": len,
"range": range,
# ⚠️ 关键:显式控制允许使用的内置函数
}
safe_locals = locals_dict or {}
# 启用追踪
old_trace = sys.gettrace()
sys.settrace(trace_calls)
try:
exec(code_obj, safe_globals, safe_locals)
finally:
# 确保追踪被关闭(即使异常也恢复)
sys.settrace(old_trace)
# 使用示例
safe_trace_exec(code_obj)
3. 进阶:AST 层面预检(防御恶意构造)
对高安全要求场景(如在线编程评测),可在 compile() 前解析 AST,拦截危险节点:
class SafeASTVisitor(ast.NodeVisitor):
def visit_Import(self, node):
raise ValueError("Import statements not allowed")
def visit_ImportFrom(self, node):
raise ValueError("ImportFrom statements not allowed")
def visit_Call(self, node):
if isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec", "__import__"):
raise ValueError("Dangerous built-in calls prohibited")
self.generic_visit(node)
# 验证源码安全性
try:
tree = ast.parse(source_code)
SafeASTVisitor().visit(tree)
except (ValueError, SyntaxError) as e:
raise RuntimeError(f"Unsafe or invalid code: {e}")
✅ 总结与最佳实践
-
永远避免
eval()/exec()直接执行用户输入;优先使用compile()+ 受限exec(); -
追踪必须配对启停:使用
try/finally确保sys.settrace(None)或恢复旧 trace 函数; -
命名空间最小化:清空
__builtins__,仅按需注入白名单函数; -
结合 AST 静态分析:可提前拦截
os.system、subprocess、open等高危操作; - 若需深度变量追踪(如每次
x = ...都捕获),应扩展trace_calls处理"line"事件并解析当前行 AST,或改用ast.NodeTransformer注入日志语句(即源码插桩)。
此方案兼顾安全性、可观测性与工程可控性,是构建 Python 在线调试器、教学追踪工具或沙箱执行引擎的推荐基础架构。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











