Keras与Skops安全模式漏洞解析:模型序列化中的任意代码执行风险

发布时间:2026/5/25 20:33:51

Keras与Skops安全模式漏洞解析:模型序列化中的任意代码执行风险 1. 项目概述当“安全模式”不再安全在机器学习项目的日常协作和部署中模型共享是一个再普通不过的环节。无论是团队内部传递一个训练好的图像分类器还是在Hugging Face Hub上发布一个开源的大语言模型我们都需要将模型从内存中的复杂对象转换成一个可以存储、传输的文件。这个过程就是序列化与反序列化。长久以来社区里流传着一种“常识”使用基于数据的格式比如JSON、HDF5来保存模型比那些基于代码的格式比如Python的pickle要安全得多。毕竟JSON文件里只是一堆文本数据看起来人畜无害。基于这种认知主流框架也推出了相应的“安全”功能。Keras框架的safe_mode和Skops库的“安全持久化”机制就是其中的典型代表。它们的设计初衷是试图在保持模型灵活性的同时为反序列化过程筑起一道安全围墙。许多开发者包括曾经的我都曾对这些标志抱有信任认为只要打开了safe_modeTrue或者使用了Skops格式从网上下载的模型文件就是相对安全的。然而最近一系列深入的安全研究包括对Keras和Skops核心代码的手动逆向工程彻底颠覆了这一认知。研究发现在这两个宣称提供安全加载机制的框架中存在至少六个零日漏洞均已获得CVE编号每一个都可能导致在模型加载阶段执行任意代码。更令人深思的是这些漏洞的根源并非高深的攻击手法而恰恰源于安全机制设计本身的缺陷和验证逻辑的不完备。与此同时像Hugging Face Hub这样集成了安全扫描工具的平台其检测能力在面对这些新型框架级漏洞时也显得力不从心出现了明显的漏报和误报。这篇文章我将从一个一线开发者和安全研究者的双重角度深入拆解这些漏洞的原理、框架安全机制的局限性以及当前模型扫描工具的不足。这不是一篇危言耸听的恐吓文而是一份基于实际漏洞分析的深度技术复盘。目的是让我们重新审视模型共享过程中的安全假设理解“安全模式”背后真实的风险边界并掌握在现有生态下更稳妥的实践策略。2. 框架安全机制的设计理念与固有局限在深入漏洞细节之前我们必须先理解Keras和Skops所宣称的安全机制究竟试图解决什么问题以及它们的基本设计思路。这有助于我们看清漏洞为何会产生以及它们是否意味着整个设计方向的失败。2.1 数据格式 vs. 代码格式安全幻觉的源头传统的Pythonpickle格式之所以危险是因为它本质上是在序列化代码执行路径。它保存了如何重建一个对象的一系列指令这些指令在反序列化时会直接映射到Python解释器的底层操作。一个恶意的pickle文件可以包含调用os.system或eval的指令从而在加载时执行任意命令。Keras的.keras格式和Skops的.skops格式则选择了另一条路它们序列化的是模型的数据结构。一个Keras模型被保存为一个ZIP压缩包里面包含用JSON描述的模型架构config.json和存储权重的二进制文件。Skops也类似它使用JSON来定义对象的类型和状态。这种设计的宣传点是JSON是数据不是代码。因此攻击者无法直接将恶意代码“写”进文件。安全性转而依赖于加载器loader在将JSON数据“翻译”回Python对象时所施加的严格限制。例如Keras的safe_mode会限制只能从受信任的Keras内部模块导入类Skops则会要求用户明确批准allowlist那些非内置的、可能不安全的类型。这里的核心误区在于将“文件格式”与“安全属性”直接划等号。实际上安全与否完全取决于加载器在解析这些数据时是否忠实地、无歧义地、且无漏洞地将数据映射回预期的、安全的对象。JSON描述的“layer”: {“class_name”: “Dense”, …}最终还是要被加载器解释为keras.layers.Dense这个类的实例化过程。如果加载器的验证逻辑有漏洞攻击者就可以通过精心构造的JSON数据诱使加载器执行非预期的操作比如导入subprocess模块并执行run函数。此时数据格式就成了攻击载荷的载体其危险性并不亚于直接的代码序列化。注意不要被“数据格式”这个词迷惑。任何需要被解释器或加载器动态解析、并用于重建运行时对象的数据格式其安全性都完全取决于解析器的实现。XML外部实体XXE攻击就是数据格式XML被滥用的经典案例。2.2 Kerassafe_mode白名单机制的理想与现实Keras的safe_mode在keras.saving.load_model函数中是其安全加载的核心。当启用时它试图实施一个白名单策略。其理想的工作流程是解析config.json找到需要实例化的层或对象的类名如“Dense”。将该类名解析为具体的Python类。在safe_mode下它只允许从一组受信任的模块主要是keras自身的模块中导入。使用JSON中提供的参数实例化该类。这个设计的初衷是好的通过限制类的来源防止加载器导入并实例化像subprocess.run这样的危险函数。然而其实现中的验证逻辑却存在多处纰漏我们发现的漏洞KV.1和KV.2正是利用了这些纰漏。KV.1的根源不充分的类名解析验证最初的safe_mode验证存在一个关键盲点。它检查了最终解析到的对象是否是一个“可调用对象”callable但却没有足够严格地检查这个可调用对象的类型和来源。攻击者可以构造一个config.json其中“class_name”指向一个完全合法的Python内置函数例如“builtins.exec”或“subprocess.run”。由于这些函数本身就是可调用对象且在某些情况下可能被意外地或通过其他间接引用纳入可解析的路径中旧的验证逻辑会允许它通过。# 简化版恶意 config.json 结构示意非完整PoC { “model”: { “class_name”: “Model”, “config”: { “layers”: [ { “class_name”: “subprocess.run”, // 攻击点类名指向危险函数 “config”: { “args”: [“/bin/sh”], “shell”: true } } ] } } }当Keras加载器试图实例化这个“层”时它实际上会直接调用subprocess.run([“/bin/sh”], shellTrue)导致任意命令执行。这个漏洞CVE-2025-1550直接动摇了safe_mode的基础承诺它未能阻止非Keras代码的执行。官方修复与KV.2的迂回攻击针对KV.1Keras 3.9版本进行了修复强化了验证逻辑采用了更严格的白名单基本上只允许导入keras.*下的模块。然而KV.2CVE-2025-9906展示了在安全机制中常见的“道高一尺魔高一丈”。攻击者不再直接导入外部模块而是转向代码重用Code Reuse攻击类似于二进制漏洞利用中的“ROPReturn-Oriented Programming”链。Keras的Lambda层允许嵌入小的Python函数以字节码或源码形式。修复后的safe_mode对Lambda层的内容进行了限制。但攻击者发现可以利用Lambda层来调用Keras内部的、已加载到内存中的工具函数。例如Keras内部有一个用于处理加载逻辑的实用函数。通过精心构造的Lambda层攻击者可以调用这个内部函数并传递参数使其行为类似于exec或eval。更巧妙的是攻击链中甚至可以包含一个步骤先调用某个内部函数来动态地将safe_mode标志设置为False从而完全解除后续的所有限制。这就像是用堡垒内部的钥匙从里面打开了堡垒的大门。这个漏洞揭示了一个更深层的问题在一个庞大、复杂的框架代码库中很难确保每一个内部函数都是“无害”的。当攻击者可以将这些函数像积木一样组合起来即利用“gadgets”时即使有白名单也可能构造出意外的执行路径。2.3 Skops安全持久化类型验证的边界与逃逸Skops为scikit-learn模型设计其安全模型基于类型验证和用户显式批准。它的.skops文件同样包含一个schema.json用图结构描述对象。其安全核心是get_untrusted_types()和load(trusted)流程加载时Skops会检查模型中所有用到的类型将不在内置可信列表中的类型报告给用户由用户决定是否批准加载。SV.1滥用MethodNode进行对象图遍历MethodNode是Skops中一个强大的特性它允许通过点号表示法如obj.attr访问对象的属性。其设计意图是安全地调用对象的方法。然而其验证逻辑存在缺陷。攻击者可以这样构造攻击链首先让Skops加载一个看似无害的、用户可能会批准的类型例如builtins.int一个整数。然后通过一连串的MethodNode从这个整数对象开始遍历Python的对象图。例如obj - __class__ - __base__ - __subclasses__。这条链可以最终访问到__builtins__模块从而获得eval或exec函数。关键漏洞在于Skops在验证时只检查了MethodNode中声明的__module__和__class__即builtins.int但没有严格验证后续通过func字段指定的属性访问链如__class__是否与初始对象的真实属性匹配。攻击者可以在func中写入任何属性名实现“挂羊头卖狗肉”。# 简化版恶意 schema.json 片段SV.1思路 { “__type__”: “MethodNode”, “__module__”: “builtins”, “__class__”: “int”, // 验证时看到的是无害的 int “func”: “__class__”, // 但实际访问的是这个属性开始了遍历链 “args”: [0], “next”: { // 链式调用下一个 MethodNode “__type__”: “MethodNode”, “__module__”: “builtins”, “__class__”: “type”, “func”: “__subclasses__”, “args”: [] // ... 后续可以连接到 __builtins__.eval } }这样一来用户批准加载的只是一个“整数”但实际执行的却是通过这个整数对象为跳板最终触发的任意代码执行。SV.2OperatorFuncNode的模块验证绕过OperatorFuncNode用于调用Pythonoperator模块中的函数如operator.add。其验证逻辑本应检查__module__是否为“operator”。但实现中出现了校验不一致get_untrusted_types()报告给用户的是完整的__module__.__class__字符串如“operator.add”而实际的加载逻辑却忽略了__module__字段只根据__class__字段的值如“add”去operator模块中查找函数。攻击者可以利用这个不一致将__module__设置为一个看似无害的、用户容易批准的值如“numpy”而将__class__设置为“call”。用户看到“numpy.call”可能会批准以为是个NumPy函数但Skops加载时却会执行operator.call。operator.call的功能是调用一个可调用对象如果攻击者能通过其他节点控制这个对象和参数就能实现任意函数调用进而执行代码。这两个Skops漏洞CVE-2025-54413, CVE-2025-54412的共同点在于它们都利用了验证阶段与执行阶段的信息不对等。展示给用户审批的信息“这是一个整数类型”与实际执行时的能力“通过这个整数访问整个运行时环境”严重不符使得用户审批机制形同虚设。2.4 遗留格式的“静默杀手”兼容性压倒安全性无论是Keras对旧版HDF5格式的支持还是Skops在模型卡片Card功能中静默回退到joblib的逻辑都反映了一个现实问题对遗留功能的兼容性需求常常会悄然绕过新建立的安全防线。KV.3被忽略的safe_modeKeras的HDF5格式是旧版Keras 2的默认保存格式。当用户使用load_model(…, safe_modeTrue)加载一个.h5文件时他们理所当然地认为安全模式会生效。但事实上这个safe_mode参数在内部根本没有传递给HDF5格式的加载例程。加载过程会无声无息地以不安全的方式进行。更糟糕的是HDF5格式对Lambda层的内容没有任何限制可以直接序列化任意Python字节码。这意味着攻击者只需将恶意模型保存为.h5格式就能轻易绕过所有safe_mode的保护而用户却得不到任何警告。这个漏洞CVE-2025-9905与其说是技术漏洞不如说是设计缺陷和不良用户体验。它破坏了用户对API行为的信任预期。修复方式不是去“修补”HDF5加载器的安全性这几乎不可能而是将safe_mode标志传递给旧加载器并使其能对Lambda层施加同样的限制或者在无法保证安全时明确抛出警告。SV.3模型卡片的危险回退Skops的Card功能用于生成模型文档。当创建一个Card对象并关联一个模型文件时Card.get_model()会被调用来加载模型。如果提供的文件是有效的.skopsZIP包则走安全加载路径。但如果文件不是有效的ZIP包例如它是一个伪装成.skops的pickle文件Skops会静默地回退到使用不安全的joblib.load()来加载它。这个行为极其危险因为它依赖于文件的实际内容而非扩展名。攻击者可以将一个恶意pickle文件命名为model.skops。用户和自动化工具看到.skops扩展名可能认为它是安全的。Skops在尝试用ZIP格式打开失败后却悄悄用joblib基于pickle加载了它导致任意代码执行。这个漏洞CVE-2025-54886的修复方案是在回退到joblib时要求用户通过显式的allow_pickleTrue参数来授权将风险决策交还给用户。这两个案例给我们的教训是任何安全机制如果存在一条可以绕过它的、默认开启的“兼容性”路径那么该机制的有效性就大打折扣。框架开发者必须在安全性和向后兼容性之间做出更明确的权衡至少要通过显式选项和清晰警告来告知用户风险。3. 模型扫描工具的局限性并非万能护盾当框架级别的安全机制失效时人们自然会将希望寄托于模型共享平台如Hugging Face Hub集成的安全扫描工具。这些工具被宣传为在模型上传时提供额外的安全层。然而我们的评估表明它们的能力存在显著局限。3.1 扫描工具的工作原理与固有短板Hugging Face Hub集成了多个扫描器主要包括Picklescan专门检测Pickle文件中的恶意操作码opcode。它对非Pickle格式的文件基本无效。ClamAV传统反病毒引擎用于检测已知的恶意软件签名。它对针对ML框架的定制化攻击载荷检测能力很弱。Protect AI Guardian一个专于ML模型威胁的扫描器定义了一系列针对特定框架如Keras、PyTorch的威胁规则。JFrog Scanner提供针对软件供应链的安全扫描包括对某些模型格式的检查。这些扫描器本质上属于静态分析工具。它们通过以下方式工作签名/模式匹配检查文件内容中是否包含已知的危险字符串、代码模式或文件结构如Picklescan找特定的opcodeProtect AI匹配其定义的威胁规则。格式解析与启发式分析尝试解析模型文件检查其结构是否异常是否包含可疑的层或操作。其局限性是显而易见的覆盖范围有限它们通常只针对少数流行的、预先定义好的文件格式和框架威胁。对于Skops.skops这种较新的格式在我们的测试中没有任何扫描器提供支持。应对新型漏洞乏力扫描器依赖已知的威胁特征签名。对于像KV.1、KV.2、SV.1、SV.2这样的新型框架逻辑漏洞在漏洞公开、扫描器更新规则之前它们几乎无法检测。我们的PoC在漏洞公开数周后上传部分扫描器仍然未能识别。误报与漏报Protect AI和JFrog Scanner对于包含Lambda层的Keras模型无论其是否恶意都倾向于标记为“可疑”或“不安全”。这产生了大量误报可能导致“警报疲劳”使用户忽视真正的威胁。同时对于某些漏洞如KV.2扫描器甚至无法完成扫描状态卡在“排队”。3.2 实测结果分析安全标签的误导性我们将针对Keras和Skops漏洞的PoC模型上传到Hugging Face Hub观察扫描结果测试模型 (格式)PicklescanClamAVProtect AIJFrog最终标签KV.1 (.keras)非pickle无问题不安全无问题不安全KV.2 (.keras)非pickle无问题可疑(卡住)(卡住)KV.3 (HDF5)非pickle无问题(无结果)不安全不安全SV.1 (.skops)非pickle无问题无问题非模型安全SV.2 (.skops)非pickle无问题无问题非模型安全SV.3 (pickle)非pickle无问题不安全不安全不安全关键发现格式歧视Picklescan仅对特定扩展名如.pkl,.pt的文件进行深度扫描。将恶意pickle文件重命名为.skops或.keras即可轻松绕过。这说明攻击者可以通过简单的文件重命名来规避检测。对新漏洞的盲区对于Skops的SV.1和SV.2漏洞所有扫描器均未报告问题。Protect AI显示“无问题”JFrog显示“非模型”。由于没有扫描器支持.skops格式Hugging Face Hub最终给出了**“安全”标签。这是最危险的漏报**它给用户传递了完全错误的信心。粗糙的规则与误报Protect AI对KV.2的检测结果是“可疑”但其规则似乎是基于“模型包含Lambda层”这一简单特征而非Lambda层内的具体行为。这导致包含良性Lambda层的模型也会被误报。这种“宁可错杀”的策略虽然谨慎但降低了工具的可信度。不一致与不可靠JFrog扫描器对KV.2的扫描任务一直处于“排队”状态无法完成。这种不稳定性在实际安全流程中是不可接受的。最终标签的聚合逻辑Hugging Face Hub会取所有扫描器结果中最严重的一个作为最终标签。这虽然能弥补单个扫描器的漏报如KV.1仅被Protect AI检出但最终标签为“不安全”但也放大了误报良性Lambda层模型被标为“不安全”。更重要的是当所有扫描器都对某种格式“无结果”或“不支持”时平台会错误地给出“安全”结论。3.3 对平台与用户的启示对于模型共享平台而言当前的扫描工具生态是碎片化且不成熟的。它们更像是“已知威胁检测器”而非“模型安全验证器”。平台需要提高透明度明确告知用户哪些格式被支持扫描哪些没有。对于未扫描或无法扫描的模型应显示“未知风险”或“未扫描”而非“安全”。推动标准化需要社区共同努力建立更统一的模型安全元数据标准和更强大的动态/行为分析工具而不是仅仅依赖静态签名。对于用户而言绝不能将平台的安全标签视为绝对的安全保证。它只是一个参考尤其是在面对新型、小众或自定义模型格式时。必须建立这样的认知从不可信来源加载模型本质上与运行不可信代码同等危险。4. 实践指南在脆弱生态中安全地共享与加载模型既然框架的安全模式和平台的扫描工具都不完全可靠作为一线开发者我们应该如何应对以下是我根据研究和实践经验总结的建议。4.1 模型提供方的最佳实践如果你是模型的创建者和分享者你的目标是最大限度地降低用户的风险并建立信任。优先使用约束性更强的交换格式如果可行考虑使用ONNX格式导出模型。ONNX定义了一个相对固定、有限的算子集合其运行时环境通常比完整的Python框架更受限从设计上减少了攻击面。虽然它可能不支持某些自定义层或最新操作但对于许多常见的模型它是一个更安全的选择。如果使用框架原生格式请附带完整代码对于PyTorch强烈建议使用torch.save(model.state_dict(), …)仅保存权重并同时提供用于重建模型架构的源代码文件.py。在文档中明确要求用户使用weights_onlyTrue加载权重然后用自己的代码实例化模型。weights_only模式是PyTorch目前最可靠的“安全”选项因为它只允许加载张量数据。对于Keras类似地可以保存为.weights.h5仅权重或使用model.save(…, save_format“tf”)SavedModel格式相对更复杂但可移植。避免分享包含自定义Lambda层或复杂自定义对象的.keras或.h5完整模型文件除非绝对必要。提供可复现的脚本提供一个清晰的README.md和一个load_model.py脚本展示如何安全地加载权重和实例化模型。详细说明与风险提示在模型卡片中明确声明所使用的框架和版本。保存的格式如.keras,.h5,.skops,.pth。是否包含自定义层、Lambda层或任何可能触发安全警告的组件。建议的安全加载步骤。警告告知用户从不可信来源加载任何序列化模型都存在潜在风险。4.2 模型使用方的防御策略如果你需要加载他人共享的模型必须采取更谨慎的态度。建立“零信任”心态默认任何从互联网下载的模型文件都是潜在的威胁载体。不要盲目信任任何“安全模式”或“安全”标签。隔离环境加载使用沙盒/容器始终在隔离的环境中首次加载和测试未知模型例如Docker容器、虚拟机或独立的云实例。确保该环境没有敏感数据、网络权限受到限制。使用专用的、无特权的用户在沙盒内使用一个非root、权限最小的用户来运行加载代码。代码审查与手动验证优先选择“权重代码”的分享方式如果提供方给出了权重文件和模型定义代码这是最安全的方式。仔细审查提供的模型定义代码确保没有可疑操作。审查模型结构如果必须加载完整模型文件在沙盒中加载后首先打印并审查模型结构如model.summary()。检查是否有来源不明或名称奇怪的层。警惕Lambda层和自定义对象对于Keras模型要特别警惕Lambda层。检查其内容是否被序列化如果是则风险极高。对于Skops模型仔细查看get_untrusted_types()报告的所有类型并彻底理解每一个。版本与格式检更新框架确保你使用的Keras、Skops等框架是最新版本已包含已知漏洞的修复。警惕遗留格式尽量避免加载旧的.h5Keras或纯pickle/joblib文件。如果必须加载明确知晓风险并在高度隔离的环境中操作。利用扫描工具但不依赖将Hugging Face等平台的扫描结果作为一个初步的、高误报率的筛选工具。如果模型被标记为“不安全”需要高度警惕。但如果标记为“安全”绝不意味着可以放松警惕尤其是对于.skops等格式。4.3 给框架与平台开发者的建议安全设计应默认拒绝安全机制如safe_mode的默认状态应该是“开”并且其关闭应该需要显式操作和明确的风险确认。对于不安全的遗留路径如HDF5加载应给出无法忽略的警告甚至默认禁用需要用户通过allow_unsafe_legacyTrue这样的参数显式开启。验证与执行必须一致像Skops那样让用户审批类型的机制其展示给用户的信息“将要加载的类型A”必须与实际执行时发生的行为“通过类型A访问了能力B”完全一致。任何偏差都会导致信任崩塌。缩小攻击面是根本鼓励并提供易于使用的“仅权重”导出选项。像PyTorch的weights_only这样的设计方向是正确的它通过极大地限制可反序列化的类型来从根本上减少风险。清晰的文档与沟通在safe_mode等功能的文档中必须明确说明其局限性“此模式旨在阻止常见攻击但不能保证完全安全。加载不受信任的模型始终存在风险。” 避免使用可能产生误导的绝对化词汇。5. 总结与反思模型安全是过程而非特性回顾Keras和Skops的这系列漏洞以及扫描工具的现状我们可以得出几个核心结论首先不存在“绝对安全”的模型序列化格式。无论是数据格式还是代码格式安全性的重担都落在了加载器的实现上。而复杂的加载器不可避免地会存在漏洞。将安全寄托于某个“安全模式”开关是一种危险的天真。其次安全是一种系统性属性而非某个孤立特性。它涉及格式设计、加载器实现、用户交互、平台检测等多个环节。当前ML模型共享生态中这些环节是割裂的框架提供了有缺陷的“安全”选项平台提供了不完整的扫描而用户则被夹在中间可能获得虚假的安全感。最后作为从业者我们必须提升自身的安全素养。这意味着要理解我们所使用工具的安全模型及其局限要习惯在隔离环境中工作要对来自外部的任何代码包括以模型形式存在的代码保持审慎。在机器学习工程化日益成熟的今天模型安全必须成为MLOps流程中不可或缺的一环而不是事后才考虑的附加项。这项工作也让我深刻体会到在追求模型表达力、易用性和向后兼容性的同时筑牢安全防线是一项持续且艰巨的挑战。它需要框架开发者更严谨的设计需要安全研究者的持续审计更需要广大用户建立正确的安全预期和实践。希望这篇深入的分析能帮助你在下一次保存或加载模型时做出更明智、更安全的选择。

相关新闻