【Bug已解决】Security: Arbitrary Module Import via Malicious Adapter Config (CWE-94) 解决方案
【Bug已解决】Security: Arbitrary Module Import via Malicious Adapter Config (CWE-94) 解决方案一、现象长什么样PEFT 的 adapter 由两个文件组成权重文件adapter_model.safetensors或adapter_model.bin和配置adapter_config.json。社区里经常直接下载别人分享的 LoRA 权重然后一行PeftModel.from_pretrained(base, someone_else_lora)就加载了。安全隐患在两种情况下会暴露你加载的是一个.binpickle格式的 adapterPython 的pickle.load在反序列化时会执行权重文件里携带的任意代码——对方只要在保存时把一段__reduce__payload 塞进.bin你一加载本机就执行了对方的代码你加载的adapter_config.json里peft_type或某个自定义字段指向了一个不在白名单内的模块路径PEFT 在解析配置时会用importlib.import_module去动态导入这个名字攻击者在配置里写peft_type: os.system之类就能触发非预期的导入甚至执行配置里带有auto_mapping或base_model_name_or_path指向一个需要trust_remote_codeTrue的仓库加载链路会去拉取并执行远程modeling_*.py里的代码更隐蔽的是adapter_config.json的revision、quantization_config字段诱导你用特定参数重新实例化基座间接执行远程代码。这对应 CWE-94「Improper Control of Generation of Code (Code Injection)」不可信的 adapter 配置参与了解析/导入流程导致任意模块导入乃至代码执行。二、背景要理解这个漏洞得知道 PEFT 加载链路里哪些地方会“按字符串去 import”pickle 反序列化torch.load默认用 pickle。pickle 的UNPickler.find_class会按配置里的类名去import并在对象重建时调用。任何.bin文件本质上是一段可执行的 Python 对象图。这是老生常谈的供应链风险。peft_type字符串到类的映射PEFT 内部有一个 registryLoraConfig→LoraModel、PrefixTuningConfig→PrefixTuningModel等。加载时它根据peft_type字符串查表并实例化对应 tuner 类。如果攻击者能注入一个不在官方表中的字符串且代码路径对它做了importlib.import_module(peft_type)就会尝试导入任意模块。auto_mapping/base_model_name_or_path部分配置会记录“这个 adapter 原本挂在哪个基座上”加载器可能据此去拉基座的config.json并触发AutoConfig/AutoModel的远程代码路径trust_remote_code。自定义 tuner 注册用户通过PeftType注册自定义方法时注册表里存的是类对象若从磁盘反序列化配置后直接用字符串重建扩展点就变成了攻击面。结论加载 adapter 不是“读数据”而是一次带代码执行语义的配置解析。一旦 adapter 来自不可信来源就必须把它当成在跑别人的脚本对待。下面用可运行代码演示“为什么.bin危险”以及“如何安全地只认 safetensors 白名单”。三、根因根因可以归纳为两条反序列化即执行pickle 格式的权重在torch.load时执行对象图里的代码这是语言层特性不是 PEFT 的 bug但 PEFT 默认允许.bin加载等于默认放行了这条攻击链。配置驱动的动态导入缺少白名单当加载逻辑根据adapter_config.json里的字符串字段去import_module/ 查 registry 时如果没有“只允许已知枚举值”的校验攻击者构造的非法字段就会变成导入入口。所以安全的加载必须做两件事只用 safetensors纯张量、无代码执行、并且对adapter_config.json的每个字段做白名单校验绝不按未知字符串去 import。四、最小可运行复现下面演示“恶意.bin在加载时执行代码”的原理仅本地演示不造成破坏以及如何用 safetensors 规避。import torch import os import pickle # ---- 演示pickle 能在反序列化时执行代码 ---- class Evil: def __reduce__(self): # 反序列化时执行在 /tmp 写一个标记文件证明“代码被执行了” return (os.system, (echo pwned /tmp/peft_poc_marker,)) # 攻击者把这样一个对象写进 .bin torch.save({lora_A.default.weight: torch.randn(4, 16), _payload: Evil()}, /tmp/evil_adapter.bin) print(加载前标记是否存在:, os.path.exists(/tmp/peft_poc_marker)) # 受害者一加载代码就跑了 _ torch.load(/tmp/evil_adapter.bin) print(加载后标记是否存在:, os.path.exists(/tmp/peft_poc_marker)) # 输出 True —— 说明 .bin 在 load 时已执行了攻击者的命令 # ---- 正确的做法用 safetensors它只存张量不执行任何代码 ---- from safetensors.torch import save_file, load_file tensors {lora_A.default.weight: torch.randn(4, 16)} save_file(tensors, /tmp/safe_adapter.safetensors) loaded load_file(/tmp/safe_adapter.safetensors) # 不会执行任何代码 print(safetensors 加载结果键:, list(loaded.keys()))运行后你会看到加载.bin之后/tmp/peft_poc_marker出现了证明 pickle 路径确实会执行代码而load_filesafetensors只返回纯张量没有任何副作用。五、解决方案第一层最小直接修复最直接的三条硬规则修复 1只用 safetensors禁用 pickle 加载import torch from safetensors.torch import load_file import os def safe_load_adapter_weights(path): bin_path os.path.join(path, adapter_model.bin) st_path os.path.join(path, adapter_model.safetensors) if os.path.exists(bin_path) and not os.path.exists(st_path): raise RuntimeError( f检测到 pickle 格式 {bin_path}出于安全原因拒绝加载。 f请要求对方提供 safetensors 版本。 ) return load_file(st_path) # 只走 safetensors修复 2强制weights_onlyTrue即便万一是.bin也要加上weights_only限制禁止执行任意对象state torch.load( /tmp/evil_adapter.bin, map_locationcpu, weights_onlyTrue, # 关键只反序列化张量/基础类型 )注意weights_onlyTrue在较新 PyTorch 上可用它会拒绝Evil.__reduce__这类对象直接抛异常。修复 3不要对任意来源开trust_remote_code# 危险加载不可信基座时开了远程代码 # model AutoModel.from_pretrained(some/repo, trust_remote_codeTrue) # 安全只用本地已审计的基座 model AutoModel.from_pretrained(/local/audited/base)六、解决方案第二层结构性改进当团队需要频繁引入外部 adapter应当从结构上把“不可信加载”堵死。改进 1adapter_config.json 白名单校验import json import os ALLOWED_PEFT_TYPES { LORA, PREFIX_TUNING, PROMPT_TUNING, P_TUNING, ADALORA, IA3, LOHA, LOKR, DELTA_TUNING, POLYTLORA, MULTITASK_PROMPT_TUNING, LN_TUNING, } def validate_adapter_config(path): cfg_path os.path.join(path, adapter_config.json) with open(cfg_path) as f: cfg json.load(f) peft_type cfg.get(peft_type) if peft_type not in ALLOWED_PEFT_TYPES: raise ValueError( fadapter_config.json 的 peft_type{peft_type!r} 不在白名单 f疑似恶意配置已拒绝。 ) # 拒绝任何会触发远程代码的字段 for dangerous in (trust_remote_code, auto_mapping, quantization_config.custom_module): if dangerous in cfg: raise ValueError(f配置含危险字段 {dangerous}已拒绝。) return cfg # 先校验再加载 validate_adapter_config(/path/to/adapter) model PeftModel.from_pretrained(base, /path/to/adapter)改进 2集中封装一个“只认可信来源”的加载函数from peft import PeftModel def load_trusted_adapter(base, path, allowed_dirNone): # 1) 只允许从指定可信目录加载 if allowed_dir is not None: ap os.path.abspath(path) if not ap.startswith(os.path.abspath(allowed_dir)): raise PermissionError(f{path} 不在可信目录 {allowed_dir} 内) # 2) 校验配置白名单 validate_adapter_config(path) # 3) 只走 safetensors safe_load_adapter_weights(path) # 4) 最后才真正加载 return PeftModel.from_pretrained(base, path)改进 3把二进制权重改成集中托管 校验和对团队内部流通的 adapter要求附带sha256清单加载前比对import hashlib def sha256_of(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(1 20), b): h.update(chunk) return h.hexdigest() expected {adapter_model.safetensors: a1b2c3...} # 来自可信渠道 actual sha256_of(/path/to/adapter/adapter_model.safetensors) assert actual expected[adapter_model.safetensors], 校验和不匹配文件可能被篡改七、解决方案第三层断言 / CI 守护把“安全加载”做成可测试的不变量。import os import json import pytest import torch ALLOWED_PEFT_TYPES {LORA, PREFIX_TUNING, PROMPT_TUNING} def validate_adapter_config(path): with open(os.path.join(path, adapter_config.json)) as f: cfg json.load(f) if cfg.get(peft_type) not in ALLOWED_PEFT_TYPES: raise ValueError(peft_type 不在白名单) def test_rejects_malicious_peft_type(tmp_path): d tmp_path / evil d.mkdir() (d / adapter_config.json).write_text( json.dumps({peft_type: os.system, r: 4}) ) with pytest.raises(ValueError): validate_adapter_config(str(d)) def test_rejects_pickle_only_adapter(tmp_path): d tmp_path / pkl d.mkdir() torch.save({w: torch.randn(2)}, str(d / adapter_model.bin)) bin_p str(d / adapter_model.bin) assert os.path.exists(bin_p) # 我们的安全加载函数应当拒绝 from safetensors.torch import load_file with pytest.raises(RuntimeError): if os.path.exists(bin_p) and not os.path.exists( str(d / adapter_model.safetensors)): raise RuntimeError(拒绝 pickle adapter) def test_allows_whitelisted_safetensors(tmp_path): d tmp_path / ok d.mkdir() (d / adapter_config.json).write_text( json.dumps({peft_type: LORA, r: 4}) ) from safetensors.torch import save_file save_file({lora_A.default.weight: torch.randn(4, 8)}, str(d / adapter_model.safetensors)) validate_adapter_config(str(d)) # 应通过这三个测试分别守护“拒绝恶意 peft_type”“拒绝纯 pickle adapter”“放行白名单内的 safetensors”。八、排查清单引入外部 adapter 前按序检查格式优先 safetensors拒绝只提供.bin的不可信来源万一是.bin加载必须带weights_onlyTrue。配置白名单打开adapter_config.jsonpeft_type必须是已知枚举任何陌生字符串都先怀疑。危险字段检查是否有trust_remote_code、auto_mapping、自定义quantization_config等会触发远程代码的字段。基座来源base_model_name_or_path是否指向你本地已审计的目录而不是随手from_pretrained一个远程仓库。可信目录把外部 adapter 放进受控目录加载函数校验路径前缀防止加载到任意路径。校验和团队内部流通的 adapter 附sha256清单加载前比对防止被中间人替换。运行环境在不具备敏感权限的沙箱/容器里加载未知 adapter即使出问题也限定爆炸半径。依赖版本保持torch/safetensors/peft为较新版本确保weights_only等保护可用。九、小结Arbitrary Module Import via Malicious Adapter Config (CWE-94)的本质是adapter 加载不是“读数据”而是一次带代码执行语义的配置解析——pickle 格式的.bin在torch.load时执行对象图里的代码adapter_config.json里的peft_type等字段会被用于动态导入。一旦 adapter 来自不可信来源就等同于在跑别人的脚本。最小修复是只用 safetensors、必要时weights_onlyTrue、不开trust_remote_code结构性改进是对adapter_config.json做白名单校验、封装只认可信目录的加载函数、用sha256校验和锁定文件最后用 CI 测试守护“拒绝恶意 peft_type、拒绝纯 pickle、放行白名单 safetensors”。把“加载不可信 adapter 执行不可信代码”这条链路在入口处彻底切断。