
本文介绍如何在不使用危险的 eval 或 exec 的前提下,对任意 Python 源码字符串(如用户提交的函数定义)进行安全的运行时跟踪,通过 AST 解析、字节码注入或 compile() + exec() 配合沙箱化上下文实现可控 tracing。
本文介绍如何在不使用危险的 `eval` 或 `exec` 的前提下,对任意 python 源码字符串(如用户提交的函数定义)进行安全的运行时跟踪,通过 ast 解析、字节码注入或 `compile()` + `exec()` 配合沙箱化上下文实现可控 tracing。
要对用户提交的 Python 代码(例如一段包含 def adder(x, y): return x + y 的源码字符串)实现精确的运行时变量追踪(如记录每次赋值、调用、返回),绝不能直接使用 eval() 或裸 exec()——正如提问者所警觉的,这会带来严重的代码注入与权限越界风险。
正确的路径是:将源码安全编译为代码对象 → 在受控命名空间中执行 → 结合 sys.settrace() 实现细粒度跟踪。关键在于隔离执行环境,并避免全局污染。
✅ 推荐方案:compile() + 安全 exec() + sys.settrace
import sys
import ast
import functools
def trace_calls(frame, event, arg):
if event == 'line':
filename = frame.f_code.co_filename
lineno = frame.f_lineno
print(f"[TRACE] {filename}:{lineno} | locals: {dict(frame.f_locals)}")
elif event == 'call':
func_name = frame.f_code.co_name
print(f"[CALL] → {func_name}")
elif event == 'return':
print(f"[RETURN] ← {arg}")
return trace_calls # 继续跟踪
def safe_trace_source(source_code: str, global_ns: dict = None) -> dict:
"""
安全执行并跟踪用户提供的 Python 源码字符串
返回执行后的局部变量快照(不含内置/危险模块)
"""
# 1. 静态验证:初步 AST 检查(可选增强安全)
try:
tree = ast.parse(source_code)
# 禁止危险节点(示例:禁止 import、open、__import__ 调用)
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
raise ValueError("Import statements are not allowed.")
if isinstance(node, ast.Call):
if (isinstance(node.func, ast.Name) and
node.func.id in ('exec', 'eval', '__import__', 'open')):
raise ValueError(f"Disallowed call: {node.func.id}")
except SyntaxError as e:
raise ValueError(f"Invalid Python syntax: {e}")
# 2. 编译为 code object(指定 filename 便于 trace 定位)
code = compile(source_code, filename="<user_input>", mode="exec")
# 3. 构建最小、受限的执行环境
safe_globals = {
'__builtins__': {
'print': print,
'len': len,
'range': range,
'list': list,
'dict': dict,
'int': int,
'float': float,
'str': str,
# ⚠️ 显式排除危险内置:open, exec, eval, __import__, etc.
}
}
if global_ns:
safe_globals.update(global_ns)
local_ns = {}
# 4. 启用跟踪并执行
sys.settrace(trace_calls)
try:
exec(code, safe_globals, local_ns)
finally:
sys.settrace(None) # 必须确保清除 trace,避免影响后续代码
return local_ns
# 使用示例
user_code = """
def adder(x, y):
z = x + y
return z
result = adder(3, 5)
"""
print("=== 开始跟踪用户代码 ===")
namespace = safe_trace_source(user_code)
print("执行后局部变量:", namespace) # {'adder': <function>, 'result': 8}</function></user_input>
? 关键注意事项
-
compile()是安全起点:它只做语法解析和编译,不执行,无副作用;相比eval()/exec()字符串直调,可控性高得多。 -
sys.settrace()作用域精准:它仅对当前线程中exec()执行的代码生效,不会污染主程序逻辑。 -
命名空间隔离是核心防线:通过显式传入
safe_globals并剔除危险内置,从根本上阻断文件读写、系统调用等攻击面。 -
AST 预检可进一步加固:如需更高安全等级(如教育平台、编程题库),可在
compile前遍历 AST 过滤subprocess,os.system,ctypes等敏感操作。 -
不要在生产环境启用
sys.settrace()全局长期监听:性能损耗显著(每行/每调用触发),应严格限定在单次exec生命周期内。
✅ 替代进阶方案(按安全性升序)
| 方案 | 安全性 | 适用场景 | 简要说明 |
|---|---|---|---|
compile() + 受限 exec()
|
★★★☆☆ | 通用教学/调试 | 如上所示,平衡安全与灵活性 |
ast.NodeTransformer + 动态重写 |
★★★★☆ | 需插桩日志/计时 | 修改 AST 插入 print() 或计数器,再 compile()
|
bytecode 库(如 bytecode 或 dis 分析+patch) |
★★★★★ | 高级性能分析 | 直接操作字节码,完全绕过源码解释风险,但复杂度高 |
? 总结:对用户代码做 tracing 的本质,不是“如何执行字符串”,而是“如何在沙箱中安全加载、监控并卸载一段逻辑”。抛弃
eval,拥抱compile+ 环境隔离 +sys.settrace,是你构建可信赖 Python 代码分析工具的坚实基础。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











