safe_eval 边界

Odoo 的 safe_eval 不是把 Python 关进笼子:server action 表达式沙箱与真实边界

很多人把 safe_eval 理解成“绝对安全的 Python 执行器”,但 Odoo 19 更准确的做法是受限求值:它限制可用 builtins、校验字节码、控制求值上下文,再把 server action 的 env、model、record、records、log 等对象显式喂进去。它很有用,但绝不是万能隔离层。

Odoo 开发 框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

很多人一看到 safe_eval 这个名字,就会自动脑补成:

  • 这是一个绝对安全的 Python 沙箱;
  • 扔进去的代码会被完全隔离;
  • 只要用了它,就可以放心让业务表达式随便跑。

这三个理解都不准确。

odoo/tools/safe_eval.pyir_actions.py 来看,Odoo 真正在做的是:

  1. 只接受字符串表达式或代码块;
  2. 限制可用 builtins;
  3. 校验编译后字节码是否落在允许集合;
  4. 只让你访问当前显式注入的上下文对象;
  5. 在 server action 场景下,再把 envmodelrecordrecordslog 等对象喂进去。

所以更准确的一句话应该是:

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() 恰恰会显式注入:

  • env
  • model
  • record
  • records
  • UserError
  • log
  • _logger

尤其 env 和 `record(s)`` 这类对象,本身就代表着 ORM 世界。

所以真正的安全边界从来不是“safe_eval 四个字”自动给你的, 而是:

  1. 允许哪些语法;
  2. 暴露了哪些对象;
  3. 这些对象本身有多强;
  4. 当前调用用户具有什么权限。

如果把这四层混成一层,就会产生最典型的错觉:

“都 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 环境里运行

只要上下文给了 envrecordrecords,表达式就不是在玩具盒里跑,而是在真实业务对象旁边跑。

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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。