开放权重,隐藏载荷:如何安全下载和加载 LLM 模型而不被入侵

Dev.to ML 2026-10-30T15:57:22.779097

本周 HN 上有两条新闻挨得很近:一条是 Mistral 公布了 Mistral Large 4 的权重,另一条是 Anthropic 被入侵并遭利用来传播远程访问木马。两件事放在一起看,结论很清楚:分发渠道越是被信任,越值得攻击者下手。现在开发者几乎每周都要往本机下载几十 GB 的模型,无脑跑一下 from_pretrained() 就完事。但一个模型文件并不只是一堆数字,取决于格式,它完全可能携带在你加载那一刻就执行的代码。本文分享我目前在用的流程,用来更安全地下载和加载开放权重模型,适用于 Hugging Face、Ollama 和 llama.cpp。

为什么模型文件能执行代码?

老熟人就是 pickle。PyTorch 旧的 .bin、.pt、.pth 格式,本质上是一个包含 pickle 数据的 zip 文件。Pickle 不只保存数据,还保存“如何重建对象”的指令,而这些指令可以是“用这些参数调用 os.system”。下面是个小例子,你可以自己验证(在虚拟机或一次性容器里跑):

import pickle, os

class Payload:
    def __reduce__(self):
        # 反序列化时,Python 会调用 os.system("id > /tmp/pwned")
        return (os.system, ("id > /tmp/pwned",))

with open("model.pkl", "wb") as f:
    pickle.dump({"weights": [0.1, 0.2], "x": Payload()}, f)

# 受害者这边:只要加载就中招
pickle.load(open("model.pkl", "rb"))
print(open("/tmp/pwned").read())

不用调用任何函数,不用 eval,光一条加载命令就够了。Hugging Face 上曾多次发现仓库里藏有这类载荷,有的开反弹 shell,有的读 ~/.aws/credentials。

除了 pickle,还有两条常被忽略的路径:

flowchart TD
    A[从 Hub 下载模型] --> B{格式?}
graph TD
    A[下载模型] --> B{文件格式}
    B -->|.bin / .pt / .pkl| C[Pickle:加载时可能执行代码]
    B -->|.safetensors / .gguf| D[仅张量 + 元数据]
    A --> E{trust_remote_code?}
    E -->|True| F[从仓库导入 .py 文件]
    C --> G[开发机 / GPU 服务器上的 RCE]
    F --> G

更安全,但仍需验证哈希。

第一步:优先选用 safetensors 和 GGUF

safetensors(Hugging Face 出品)只包含一个 JSON header 和一块原始的 tensor 字节。它没有对象构建机制,所以根本没有地方塞代码。llama.cpp 的 GGUF 也一样:先是 header,接着是键值对形式的元数据,然后是 tensor。

现在几乎所有大模型都有 safetensors 版本,社区转 GGUF 也很快。我的规则是:一个仓库里只有 .bin 却没有 .safetensors,就该当成可疑对象,新发布的模型尤其如此。

如果实在非加载旧 PyTorch 文件不可,就用 weights_only=True。从 PyTorch 2.6 起,这已经是 torch.load 的默认值,但不少旧代码库为了「消除报错」还把它覆盖成 False。在你的仓库里 grep 一下看看:

# 找出那些把保护机制关掉的地方
grep -rnE "weights_only\s*=\s*False|trust_remote_code\s*=\s*True" --include="*.py" .
# 只下 safetensors + config,彻底不碰 pickle 文件
hf download mistralai/SomeModel \
  --include "*.safetensors" "*.json" "tokenizer*" \
  --local-dir ./models/somemodel

hf 是 huggingface_hub 的新 CLI(用来替代 huggingface-cli,大约从 0.34 版起提供)。下载时就用 --include 过滤,是让 .bin 文件根本不落到磁盘上的最简单办法。

第二步:固定 revision 并校验哈希

按 main 分支名下载,就跟 pip install 不锁版本一样:今天一个样,明天一个样。Hub 上每个 commit 都有一个 SHA,每个 LFS 文件都有公开的 SHA256。这两者都要固定。

# verify_model.py — 固定 commit、下载、拿 SHA256 跟 lockfile 比对
import hashlib, json, sys
from pathlib import Path
from huggingface_hub import HfApi, snapshot_download

REPO = "mistralai/SomeModel"
REVISION = "a1b2c3d4e5f6..."  # 已审查过的 commit SHA,千万不要用 "main"

LOCK = Path("model.lock.json")

def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(8 * 1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()

def build_lock():
    info = HfApi().model_info(REPO, revision=REVISION, files_metadata=True)
    lock = {s.rfilename: s.lfs.sha256 for s in info.siblings if s.lfs}
    LOCK.write_text(json.
dumps(lock, indent=2))
print(f"Đã ghi {len(lock)} hash vào {LOCK}")

def verify():
    lock = json.loads(LOCK.read_text())
    local = Path(snapshot_download(REPO, revision=REVISION,
                 allow_patterns=["*.safetensors", "*.json"]))
    bad = [n for n, h in lock.items()
           if (local / n).exists() and sha256(local / n) != h]
    if bad:
        sys.exit(f"HASH MISMATCH: {bad}")
    print("OK: tất cả file khớp lockfile")

if __name__ == "__main__":
    build_lock() if "--lock" in sys.argv else verify()

把 model.lock.json 提交进仓库,作用类似前端的 package-lock.json。在 CI 里,或者在构建 GPU 服务器镜像时,启动推理服务前先跑一遍 python verify_model.py。要升级模型就改 REVISION,执行 --lock,审一遍 diff 再合并。

从其他渠道下载的 GGUF 文件,至少把 sha256sum 和作者公布的值对一下。

第 3 步:首次加载前先扫描,并在隔离环境里转换

有些场景绕不开 pickle 文件,比如老的内部 checkpoint、还没出 safetensors 版的研究模型。那就先扫描,再在隔离环境里一次性转换:

pip install picklescan fickling

# picklescan:列出可疑的全局导入(os.system、subprocess、builtins.exec 等)
picklescan --path ./models/legacy/pytorch_model.bin

# fickling:分析更深入,还能把 pickle 反编译成伪 Python
fickling --check-safety ./models/legacy/pytorch_model.bin
# 在没有网络、不挂载 home 目录的容器里转换
docker run --rm --network none \
  -v "$PWD/models/legacy:/in:ro" -v "$PWD/models/clean:/out" \
  pytorch/pytorch:2.6.0-cuda12.4-cudnn9-runtime \
  python -c "import torch; from safetensors.torch import save_file; \
  sd = torch.load('/in/pytorch_model.bin', weights_only=True); \
  save_file({k: v.contiguous() for k, v in sd.items()}, '/out/model.safetensors')"

--network none 让 payload 即便执行了也回连不了外部;/in 挂成只读,它改不了原始文件。走完这一步,团队以后只用 .safetensors 版本。

sequenceDiagram
sequenceDiagram
    participant Dev
    participant Hub as Hugging Face Hub
    participant CI
    participant GPU as 推理服务器

    Dev->>Hub: model_info(revision=SHA)
    Hub-->>Dev: 文件列表 + SHA256
    Dev->>CI: 提交 model.lock.json
    CI->>Hub: snapshot_download(revision=SHA, *.safetensors)
    CI->>CI: 对照 lockfile 校验 SHA256
    CI->>GPU: 部署已通过校验的镜像
    GPU->>GPU: 加载,trust_remote_code=False

关于 trust_remote_code:如果模型用了新架构,先读一遍 modeling_*.py,锁定 revision;或者等几周,让该架构进入 transformers 官方版本。大模型的架构通常很快就会得到上游支持。

结论

开放权重是好事,但「从官方渠道下载」并不等于安全。Anthropic 这次事件说明,官方渠道本身也可能被变成投毒通道。

我给所有加载模型的项目都用这份检查清单:

整个配置大概花一个下午,换来的是:GPU 服务器——那里通常放着云端 token 和客户数据——不会因为一次 from_pretrained() 就变成后门。


本周 HN 上有两条挨得很近的新闻:一边是 Mistral 公布了 Mistral Large 4 的权重,另一边是 Anthropic 被入侵、并被利用来传播远程访问木马(RAT)。两条放在一起看,结论很清楚:渠道越是被信任,越值得攻击者盯上。

开发者现在几乎每周都要拉几十 GB 的模型到本地,跑 from_pretrained() 时什么都不想。但一个模型文件不只是「一堆数字」。取决于格式,它可能含有在你加载那一刻就执行的代码。

查看原文