Python 热修复的代码注入风险

1次阅读

热修复禁用exec/eval因存在任意代码执行风险;应使用types.functiontype替换函数对象,确保逻辑来自可信模块,避免远程字符串注入。

Python 热修复的代码注入风险

python 热修复时 execeval 为什么不能直接用

热修复本质是运行时动态加载新逻辑,但用 execeval 执行远程/外部传入的字符串代码,等于主动打开任意代码执行后门。哪怕只修一行,只要输入可控,攻击者就能调 os.system、删文件、窃取环境变量。

常见错误现象:SyntaxError 反而是好事;更危险的是静默执行了恶意逻辑,比如修复函数里悄悄加了 requests.post(...) 回传内存数据。

  • 使用场景:线上紧急 patch 函数行为,比如修复某个计算逻辑 bug
  • 参数差异:传字符串 vs 传已编译的 codeobject —— 后者仍不安全,只要来源不可信
  • 性能影响:小,但安全代价远高于这点开销
  • 兼容性无问题,但所有 Python 版本都一样危险

真正可用的热修复方式:用 types.FunctionType 替换函数对象

核心思路是——只替换函数体,不执行任意字符串。把修复逻辑写在本地模块里,编译成字节码,再用 types.FunctionType 构造新函数并赋值到原位置。这样控制权始终在可信代码路径内。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • 修复代码必须提前写在项目内(如 hotfixes/fix_calc.py),不能从网络或配置中心拉字符串
  • inspect.getsource 提取函数源码 → compile 得到 codeobjecttypes.FunctionType 构造,比手写 exec 多两行,但安全边界清晰
  • 注意闭包变量:原函数引用的自由变量(nonlocal 或外层作用域变量)不会自动继承,得手动传进新函数的 globals重构为参数
  • 类方法热修复要额外处理 __func__ 和绑定逻辑,不如直接改实例的 __dict__

示例:new_func = types.FunctionType(compile(src, '', 'exec').co_consts[0], globals()) —— 注意 co_consts[0] 是假设你 compile 的是单个函数定义,实际需按 AST 解析定位

热修复后函数没生效?检查这三个地方

不是代码写错,而是 Python 的名字绑定机制让人误以为“改了就生效”。真实情况是:引用关系没断,旧函数还在被调用。

  • 模块级函数:确认你替换的是被调用方看到的那个名字,比如 from module import func 后再 module.func = new_func 不生效,因为导入时已绑定到本地符号表
  • 类方法:直接改 class.method = new_func 只影响新实例;已有实例的方法绑定已固化,得遍历 instance.__dict__ 或重设 instance.__class__.method
  • 装饰器干扰:如果原函数被 @lru_cache@Property 包裹,热修复后缓存未清、描述符逻辑未更新,表现仍是旧行为

别忽略 importlib.reload 的副作用

有人图省事直接 importlib.reload(module),觉得“重载整个模块”最彻底。但它会创建全新模块对象,而老模块里的对象(如全局 dict、单例类实例、线程局部存储)不会自动迁移,极易引发状态不一致。

典型症状:AttributeError 找不到刚 reload 过的属性,或者两个模块版本的类实例互相不认(isinstance(obj, NewClass) 返回 False)。

  • reload 前必须确保:无长期持有老模块对象的引用(比如注册在 atexit 或信号 handler 里的函数)
  • reload 后必须手动同步状态:比如把老模块的 CACHE_DICT 内容 copy 到新模块对应位置
  • 某些 C 扩展模块(如 numpycv2)根本不能 reload,会直接 segfault

复杂点在于:热修复从来不只是“换一段逻辑”,而是“在不中断服务的前提下,让新旧状态平稳交接”。这个交接过程,比注入代码本身更难把控。

text=ZqhQzanResources