三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Bug已解决】[Security] Incomplete Fix for CVE-2026-44513: community Pipeline Branch Bypasses trust_remo…

【Bug已解决】[Security] Incomplete Fix for CVE-2026-44513: community Pipeline Branch Bypasses trust_remo…

【Bug已解决】[Security] Incomplete Fix for CVE-2026-44513: community Pipeline Branch Bypasses trust_remote_code Check 解决方案

一、现象长什么样

diffusers 的trust_remote_code机制是用来防止加载不可信自定义流水线(community pipeline)时执行任意代码的。按设计,只要你没有显式写trust_remote_code=True,加载任何需要远程代码的 pipeline 都必须被拦下来并抛错:

# 期望行为:trust_remote_code 没开,应该被拦 ValueError: Loading ... requires you to execute the modeling file in the repo. Make sure you have read the code and are confident it is safe to run. You can avoid this by passing `trust_remote_code=True`.

但 CVE-2026-44513 的修复是不完整的:当加载路径走某个特定分支(比如本地已经有缓存、或走custom_pipeline字符串而非从 hub 拉取)时,trust_remote_code的检查被跳过了,于是即使trust_remote_code=False远程代码仍然被执行了。现象有两种:

# 症状 A:本该抛错却静默成功,远程代码已经跑完 pipeline = DiffusionPipeline.from_pretrained( "some-community/pipeline", trust_remote_code=False) # 竟然加载成功 # 症状 B:能加载,但根本没问过用户同不同意,等于安全开关形同虚设

这是一个典型的安全回归:它不是功能崩了,而是安全闸门在某个分支上没关紧,用户以为关着的门其实开着。

二、背景

diffusers 在DiffusionPipeline.from_pretrained里有两条加载自定义流水线的路径:

  1. 标准 hub 路径:从模型仓库拉取modeling_*.py,这种会走完整的trust_remote_code校验;
  2. community pipeline 分支:通过custom_pipeline="org/name"或在缓存已存在时走本地加载,这条分支本应同样校验,但 CVE 修复只在路径 1 上加了检查,路径 2 漏了。

trust_remote_code的语义很明确:远程代码 = 会exec仓库里的 Python 文件,有执行任意代码的能力。所以加载前必须“要么用户显式同意(True),要么拒绝加载”。这是个授权检查(authorization),不是可选项。一旦某个分支绕过它,就等于任何能诱导用户加载特定 pipeline 的人都能在他机器上跑代码。

为什么是“incomplete fix”?因为修复只堵了“从网络拉取时”的那一处,没堵“本地缓存命中时”或“custom_pipeline 字符串解析时”的那一处——安全修复最忌讳的就是只补一个入口,留下并行入口。

三、根因

根因是授权检查没有收敛到单一入口,存在并行加载分支

  1. 检查只加在一个分支:CVE 补丁在get_class/ hub 下载逻辑里加了if not trust_remote_code: raise,但custom_pipeline的处理函数里有另一条直接importlib加载本地文件的路径,没加同样的守卫。

  2. 缓存命中短路了检查:当本地snapshots/缓存里已经有这份远端代码时,加载逻辑走“直接读本地文件”分支,跳过了“是否信任远程代码”的判断,因为代码已经“在本地了”。但本地这份代码正是之前从远程来的,信任状态不该因为“已经下载过”就失效。

  3. trust_remote_code默认语义被分支忽略:主路径读kwargs.get("trust_remote_code", False),但 community 分支用了另一个局部变量或默认值,导致False没传进去。

本质:这是授权检查分散在多个加载入口、且缓存状态被错误当作信任状态导致的绕过。和所有“安全修复只补一半”的问题一样,根因是缺少一个所有加载路径都必须经过的“信任闸门”。

四、最小可运行复现

下面用最小代码模拟“两个加载入口,只有一个做信任检查”的绕过(不真联网,用本地文件模拟远程代码):

importosimportimportlib.utilimporttempfile# 模拟一份“远程”自定义 pipeline 代码(危险:会执行任意语句)remote_code=""" print("[evil] remote code executed!") class DummyPipeline: pass """tmp=tempfile.mkdtemp()mod_path=os.path.join(tmp,"modeling_dummy.py")withopen(mod_path,"w")asf:f.write(remote_code)defload_via_hub_check(mod_path,trust_remote_code):"""标准 hub 路径:做了信任检查。"""ifnottrust_remote_code:raiseValueError("trust_remote_code must be True to load remote code")spec=importlib.util.spec_from_file_location("dummy",mod_path)mod=importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)# 执行远程代码returnmoddefload_via_community_branch(mod_path,trust_remote_code):"""community 分支:CVE 修复漏掉的入口,没做检查。"""# 注意:这里根本没有读 trust_remote_code,直接加载spec=importlib.util.spec_from_file_location("dummy",mod_path)mod=importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)# 远程代码被执行了!returnmod# 用户明确关掉信任trust=Falsetry:load_via_hub_check(mod_path,trust)# 正确:被拦下exceptValueErrorase:print("hub path blocked:",e)load_via_community_branch(mod_path,trust)# 绕过:直接执行了 [evil]

运行后会看到[evil] remote code executed!—— 即便trust=False,community 分支依然执行了远程代码。这就是 incomplete fix 的精确缩影。

五、解决方案(第一层:最小直接修复)

最小修复:把信任检查加进那个漏掉的分支,并在缓存命中时也重新校验,而不是因为“已经在本地”就跳过。

importimportlib.utilimportosdef_assert_trusted(trust_remote_code:bool,name:str):"""所有加载入口都必须先过的信任闸门。"""ifnottrust_remote_code:raiseValueError(f"Loading{name}requires executing remote modeling code. "f"Pass `trust_remote_code=True` only if you trust its source.")defload_via_community_branch_fixed(mod_path,trust_remote_code):_assert_trusted(trust_remote_code,os.path.basename(mod_path))# ← 补上spec=importlib.util.spec_from_file_location("dummy",mod_path)mod=importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)returnmoddefload_from_cache_fixed(mod_path,trust_remote_code):# 缓存命中不等于信任:仍然先过闸门_assert_trusted(trust_remote_code,os.path.basename(mod_path))# ... 再读本地文件

这一层改动最小:在漏掉的分支和缓存分支各加一行_assert_trusted,就能堵住绕过。但它依赖“每个新分支都记得加”,下看第二层怎么把闸门收口。

六、解决方案(第二层:结构性改进)

把“任何加载远程/自定义代码的入口都必须先过信任闸门”固化成单一事实来源。下面这个 dataclass 是信任策略的集中地:所有加载函数只通过它的require_trusted方法执行加载,从结构上保证不存在能绕过闸门的并行入口

fromdataclassesimportdataclass,fieldfromtypingimportCallable,Dict,Tupleimportimportlib.utilimportos@dataclassclassTrustRemoteCodeGuardPolicy:"""单一事实来源:集中管理 trust_remote_code 授权闸门。"""_loaders:Dict[str,Callable[[str],object]]=field(default_factory=dict)defregister_loader(self,name:str,loader:Callable[[str],object])->None:self._loaders[name]=loaderdefrequire_trusted(self,trust_remote_code:bool,label:str)->None:"""唯一授权点:信任未开启则一律拒绝。"""ifnottrust_remote_code:raiseValueError(f"Loading{label}requires executing remote code. "f"Set trust_remote_code=True only if you trust the source.")defload(self,name:str,mod_path:str,trust_remote_code:bool)->object:# 无论走 hub / community / cache 哪个分支,这里都是唯一入口self.require_trusted(trust_remote_code,name)ifnamenotinself._loaders:raiseKeyError(f"no loader registered for{name}")returnself._loaders[name](mod_path)# 注册各分支的“纯加载器”(不含信任逻辑,逻辑全在 policy.load 里)policy=TrustRemoteCodeGuardPolicy()policy.register_loader("hub",lambdap:_exec_module(p))policy.register_loader("community",lambdap:_exec_module(p))policy.register_loader("cache",lambdap:_exec_module(p))def_exec_module(mod_path:str):spec=importlib.util.spec_from_file_location("m",mod_path)mod=importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)returnmod

这一层的关键收益:

  • 单点授权require_trusted是唯一闸门,新增 hub/community/cache 任何分支都只调policy.load,不可能绕过;
  • 信任状态与缓存解耦:缓存命中也走load,所以“已经下载过”不再等于“自动信任”;
  • 单一事实来源:所有“什么情况下能执行远程代码”的约定都收口在TrustRemoteCodeGuardPolicy,安全审计只盯它。

七、解决方案(第三层:断言 / CI 守护)

把第二层的闸门钉成 pytest,挂进 CI,确保任何分支都不可能在trust_remote_code=False时执行远程代码:

importosimporttempfileimportpytestfromyour_package.trust_guardimportTrustRemoteCodeGuardPolicy,_exec_moduledef_make_remote_file(tmp_path):p=tmp_path/"modeling_dummy.py"p.write_text("class DummyPipeline:\n pass\n")returnstr(p)deftest_all_branches_blocked_when_untrusted(tmp_path):# 断言 1:hub / community / cache 三个分支在 trust=False 时全部被拦policy=TrustRemoteCodeGuardPolicy()policy.register_loader("hub",lambdap:_exec_module(p))policy.register_loader("community",lambdap:_exec_module(p))policy.register_loader("cache",lambdap:_exec_module(p))path=_make_remote_file(tmp_path)forbranchin("hub","community","cache"):withpytest.raises(ValueError):policy.load(branch,path,trust_remote_code=False)deftest_branch_allowed_when_trusted(tmp_path):# 断言 2:trust=True 时三个分支都能正常加载policy=TrustRemoteCodeGuardPolicy()policy.register_loader("hub",lambdap:_exec_module(p))policy.register_loader("community",lambdap:_exec_module(p))policy.register_loader("cache",lambdap:_exec_module(p))path=_make_remote_file(tmp_path)forbranchin("hub","community","cache"):assertpolicy.load(branch,path,trust_remote_code=True)isnotNonedeftest_cache_hit_still_requires_trust(tmp_path):# 断言 3:即便“已经在本地缓存”,未信任也必须拒绝(缓存≠信任)policy=TrustRemoteCodeGuardPolicy()policy.register_loader("cache",lambdap:_exec_module(p))path=_make_remote_file(tmp_path)withpytest.raises(ValueError):policy.load("cache",path,trust_remote_code=False)

三条断言从“三分支全拦”“信任时放行”“缓存仍须信任”三面把绕过钉死,确保 CVE 修复不再“补一半”。

八、排查清单

遇到trust_remote_code形同虚设、或自定义 pipeline 在False时仍加载时:

  1. 先确认是哪个加载分支:是custom_pipeline=字符串?还是本地缓存命中?还是从 hub 拉取?找到对应代码路径。
  2. 在该分支里搜importlib/exec_module/from_pretrained的加载点,确认前面有没有trust_remote_code判断。没有就漏了。
  3. 缓存命中分支最容易漏:检查它是否因为“文件已在本地”就跳过了信任判断。缓存状态 ≠ 信任状态。
  4. 把所有加载入口都改为只经过第二层的policy.load单一闸门,删掉散落的if trust_remote_code判断,避免多入口不一致。
  5. 跑第三层 pytest,断言hub/community/cache三个分支在False时全抛ValueError
  6. 安全修复的原则:闸门只允许有一个,且必须每个入口都过。任何“只在某一处加检查”的改法都是 incomplete fix。

九、小结

CVE-2026-44513 的 incomplete fix,本质是**trust_remote_code这个授权闸门只加在了 hub 下载分支,而 community pipeline / 本地缓存命中分支绕过了它**,导致用户明明设了trust_remote_code=False,远程代码仍被执行。根因是授权检查分散在多个加载入口、且缓存状态被错误当成信任状态。修复分三层——第一层在漏掉的分支和缓存分支各补一行信任断言,打通最小闭环;第二层用TrustRemoteCodeGuardPolicy这个 dataclass 把所有加载入口收口到唯一的require_trusted闸门,从结构上消灭并行绕过;第三层用三条 pytest 把“三分支全拦、信任时放行、缓存仍须信任”钉死在 CI。安全心法一句话:授权检查只能有一个入口,且必须每个加载路径都经过它,少一个就是 incomplete fix。

← 返回列表