先说结论
很多人一看到 safe_eval 这个名字,就会自动脑补成:
- 这是一个绝对安全的 Python 沙箱;
- 扔进去的代码会被完全隔离;
- 只要用了它,就可以放心让业务表达式随便跑。
这三个理解都不准确。
从 odoo/tools/safe_eval.py 和 ir_actions.py 来看,Odoo 真正在做的是:
- 只接受字符串表达式或代码块;
- 限制可用 builtins;
- 校验编译后字节码是否落在允许集合;
- 只让你访问当前显式注入的上下文对象;
- 在 server action 场景下,再把
env、model、record、records、log等对象喂进去。
所以更准确的一句话应该是:
safe_eval是受限求值器,不是万能隔离监狱。
一、safe_eval 防的首先不是“业务逻辑错误”,而是“表达式能力过大”
safe_eval() 的主体很短,但逻辑很关键:
- 拒绝直接传入
CodeType; - 要求
context必须是 dict; - 先
compile_codeobj(); - 再
assert_valid_codeobj(_SAFE_OPCODES, c, expr); - 最后用受限的
__builtins__执行。
这说明它第一层想解决的问题是:
- 不要让调用方随便丢一个已经编译好的任意代码对象进来;
- 不要让字节码包含超出白名单的能力;
- 不要默认继承完整 Python 内建环境。
也就是说,它更像在控制:
这段表达式“能做什么语言动作”。
而不是在保证:
这段业务代码“结果一定安全”。
这两件事差别非常大。
二、名字叫 safe,不代表上下文对象天然无害
safe_eval 很容易让人忽略一个关键事实:
- 语言能力受限,不等于上下文能力受限。
如果你给它的上下文里塞进一个非常强的对象,表达式仍然可能做很多事。
而 server action 的 _get_eval_context() 恰恰会显式注入:
envmodelrecordrecordsUserErrorlog_logger
尤其 env 和 `record(s)`` 这类对象,本身就代表着 ORM 世界。
所以真正的安全边界从来不是“safe_eval 四个字”自动给你的, 而是:
- 允许哪些语法;
- 暴露了哪些对象;
- 这些对象本身有多强;
- 当前调用用户具有什么权限。
如果把这四层混成一层,就会产生最典型的错觉:
“都 safe_eval 了,应该没什么风险。”
三、server action 里的代码不是黑箱,它有明确执行入口
在 IrActionsServer 里,代码型 server action 走的是:
def _run_action_code_multi(self, eval_context):
safe_eval(self.code.strip(), eval_context, mode="exec", filename=str(self))
return eval_context.get('action')
这段代码有三个很重要的信息:
1)是 mode="exec"
说明这不是只求一个表达式值,而是允许执行一段代码块。
2)上下文是可变的
safe_eval 最后会把执行过程中在 globals_dict 里产生的新变量回写到 context。
所以 server action 代码块不仅能读取上下文,还能改写上下文状态,例如设置 action 返回给前端。
3)文件名会挂成当前 action
filename=str(self) 让报错栈更可追踪。这个小细节说明官方知道:
这种机制不是“永远不会出错的简单配置”,而是一种需要调试的可执行系统。
四、为什么它不是“绝对安全沙箱”
1)它仍在真实 ORM 环境里运行
只要上下文给了 env、record、records,表达式就不是在玩具盒里跑,而是在真实业务对象旁边跑。
2)它是受限 Python,不是零能力 DSL
它不是把你限制到只能写 declarative 规则,而是仍允许相当多的 Python 表达式与代码块能力。
3)它主要解决“别让表达式直接变成任意 Python 武器库”
这已经很有价值,但它不等于:
- 自动理解你的业务边界;
- 自动阻止错误写库;
- 自动防止所有副作用;
- 自动替你做权限设计。
所以 safe_eval 真正的价值在于缩小攻击面与表达能力面,不是替代业务治理。
五、为什么 Odoo 还要给 server action 这套能力
因为 ERP 的一个现实需求是:
- 管理员希望在不发版的前提下配置一些自动动作;
- 某些流程确实需要“轻脚本化”;
- 规则引擎光靠 declarative 配置不够灵活。
所以 Odoo 的取舍不是“要不要可执行表达式”,而是:
- 要;
- 但不能直接裸奔执行任意 Python;
- 需要一个受控入口;
- 需要一个明确 eval context;
- 需要把能力边界尽量收拢。
这就是 safe_eval + eval_context + server action runner 这套组合真正的产品思路。
六、开发和运维最容易踩的坑
1)把 safe_eval 当成合规背书
用了它,不代表任何 server action 都适合开放给大量人配置。
2)忽视 eval context 才是真正的权限面
真正危险的往往不是表达式语法,而是你暴露了什么对象、这些对象又能做什么。
3)把复杂业务硬塞进 server action code
代码块越长、越依赖 ORM 细节、越有事务副作用,它就越接近“该下沉成模块代码”的边界。
4)把异常当成“表达式写错了”
很多报错其实不是 Python 语法问题,而是:
- 当前
record为空; - 上下文 active_model 不匹配;
- 当前用户权限不足;
- 你在受限上下文里调用了本来就不该用的能力。
七、实务上该怎么理解它
最稳妥的心智模型是:
safe_eval不是“让我安全地执行任何 Python”,而是“让我在被收窄的语法与上下文里执行一小段受控 Python”。
如果一段逻辑满足以下特征,它比较适合留在 server action:
- 短;
- 可读;
- 依赖上下文明确;
- 副作用有限;
- 出错时容易定位。
反过来,如果它开始涉及:
- 大量 ORM 写操作;
- 复杂事务一致性;
- 多对象权限穿透;
- 长链路业务编排;
那它往往已经越过了 safe_eval 最舒服的使用边界。
结语
Odoo 这套设计其实相当务实:
- 不假装“配置型自动化”可以完全不用代码;
- 也不放任代码框随便执行整套 Python 能力;
- 而是在中间做一个受限求值层。
所以别把 safe_eval 神化,也别低估它。
它真正解决的是:
让可配置表达式在可用性和风险之间,落到一个还能接受的中间地带。
记住这句最有用:
safe_eval限制的是表达式能力面;真正的系统风险,还取决于你给了它什么上下文对象、在什么权限下执行。
DISCUSSION
评论区