exec()和eval()不能直接当沙箱用,因其默认共享调用者命名空间,易触发系统调用或污染内置函数;安全做法是显式传入隔离的globals/locals、白名单控制__builtins__、必要时采用进程级隔离。

exec() 和 eval() 为什么不能直接当沙箱用
因为它们默认共享调用者的全局/局部命名空间,exec() 里一句 import os; os.system("rm -rf /") 就能触发真实系统调用——这不是隔离,是裸奔。
常见错误现象:NameError 看似安全,实则只是没碰到底层模块;更危险的是代码悄悄修改了外层变量,比如在 exec() 里给 __builtins__.open = lambda *a, **k: None,后续所有模块的文件操作都失效了,但错误发生在别处,极难定位。
- 必须显式传入干净的
globals和locals字典,且二者不能是同一个对象(否则locals()修改会污染globals()) -
globals字典至少要包含{"__builtins__": {}},否则连len()、range()都用不了 - 不要复用已有字典(如
globals().copy()),里面可能混着__import__或compile这类高危函数
如何安全地限制内置函数和模块访问
核心不是“删掉什么”,而是“只留下什么”。Python 没有原生沙箱机制,得靠白名单控制入口点。
使用场景:执行用户提交的简单计算表达式(如 "2 + 3 * x"),或预定义 API 的轻量脚本(如配置驱动的条件判断逻辑)。
立即学习“Python免费学习笔记(深入)”;
- 构造最小
__builtins__:只保留int、float、str、len、max等纯函数,彻底剔除__import__、open、exec、eval - 如果需要导入模块,必须自己封装一个受控的
import_module()函数,硬编码允许列表,禁止相对导入和动态模块名 - 注意
__builtins__在不同 Python 版本中可能是 dict 或 module 对象,统一用dict(__builtins__)转换再过滤
ast.literal_eval() 适合什么,不适合什么
它只处理字面量(123、"hello"、[1,2]、{"a": True}),不执行任意代码,天生安全。但这也意味着它根本不是“动态加载代码”的方案。
常见错误现象:用户传入 "func(x)" 或 "a + b",ast.literal_eval() 直接抛 ValueError: malformed node or string,而不是你期望的“安全拒绝”。
- 适合解析配置、JSON-like 数据、用户输入的结构化参数
- 不适合表达式计算、函数调用、任何含运算符或标识符的字符串
- 性能上比
eval()略慢,但差距可忽略;关键是它不提供执行上下文控制能力
真正隔离还得靠进程级边界
纯 Python 层面的沙箱永远存在绕过可能(比如通过 getattr 拿到类型对象再反射出危险方法)。生产环境需要硬隔离。
使用场景:运行不可信第三方插件、在线编程题评测、低权限策略脚本执行。
- 用
subprocess.run()启动独立 Python 进程,通过 stdin/stdout 通信,主进程完全不暴露解释器状态 - 配合
timeout和limit memory(Linux 下用ulimit或resource模块)防死循环和内存爆炸 - 注意 Windows 下
subprocess启动开销较大,高频调用需考虑连接池或长进程模型
最易被忽略的一点:即使做了所有 Python 层过滤,只要没禁用 __import__,攻击者就能通过 ().__class__.__mro__[1].__subclasses__() 找到 os._wrap_close 之类冷门路径,最终拿到 os 模块——进程隔离才是兜底防线。










