Most open-source model-file scanners inspect the outer container of a model
and stop there. A PyTorch .pt is a ZIP, so they open the ZIP. A .pkl is a
pickle, so they read the opcodes. That is exactly the assumption an attacker
breaks: put the dangerous payload one or more compression/archive layers deeper,
or behind a codec the scanner's standard library cannot decode. The model loader
still reaches the payload. The scanner never sees it.
This is a documented class, not a hypothetical:
-
"The Art of Hide and Seek: Making Pickle-Based Model Supply Chain Poisoning
Stealthy Again" (Liu et al., USENIX Security 2026, arXiv:2508.19774)
enumerates loading paths like
gz→pkl,tar→pkl,zip→zip→pkl,gz→tar→pkl, and measures widely used scanners at F1 ≈ 0 on several of them. -
CVE-2025-1945 / CVE-2025-1944: ZIP flag-bit / local-header manipulation
hides a pickle from the scanner while
torch.loadstill loads it. - CVE-2025-10156: a ZIP member with a bad CRC makes the scanner raise and stop — the archive is left unscanned while the loader reads it anyway.
Please verify each identifier against its upstream advisory before acting on it;
treat this post, like any web page, as untrusted until you do.
A static detector for the nesting dimension
nested_container_scan (part of the open-source keras-model-scan toolkit)
walks the container tree and reports the full nesting path to each hidden
payload, plus the members it cannot open and the codecs it cannot decode. It is a
complement to the format-specific scanners — once a payload is located, you hand
it to the matching one (pickle_model_scan, keras_model_scan, …).
It never executes, imports, unpickles or otherwise deserializes anything. It
reads a bounded prefix of each member and classifies it by magic bytes
(pickle protocol header, HDF5 signature, NumPy \x93NUMPY) or by file-name
extension, using only the Python standard library (zipfile, tarfile, zlib,
bz2, lzma).
# scan files or directories
python nested_container_scan.py downloads/ --fail-on HIGH
# machine-readable
python nested_container_scan.py model.tar.gz --json
The rules, briefly:
- NC001 (HIGH/MED) — a dangerous payload reachable only after a compression/archive path single-format scanners miss (any path of length ≥ 2, or a single non-ZIP layer). HIGH when magic-confirmed, MED when inferred from the name.
- NC002 (MED) — a container nested inside another container.
-
NC010 (MED) — a member in a codec stdlib cannot decode (
zstd,lz4), silently skipped by stdlib-based scanners. - NC011 (MED) — a member stdlib cannot open (bad CRC, bad flag bits, malformed header): a tolerant loader may still read it. This is the exact surface of CVE-2025-10156 and CVE-2025-1944.
- NC003 (LOW) — a defensive cap was hit (possible decompression bomb or anti-analysis nesting).
A payload sitting directly inside one ZIP (the ordinary .pt layout) is
deliberately not flagged — that is the case the format scanners already
handle. The signal here is the extra hiding layer, which keeps false positives
low.
Honest limits
- Detection is by magic/name, so a payload in a format the module does not recognize at a leaf is not flagged as dangerous (though its nesting and any unreadable/undecodable wrappers still are).
-
zstdandlz4members are reported, not decoded (no stdlib codec through Python 3.13) — supply the decoded bytes to rescan. - Name-based MED rules can over- or under-fire; treat them as a prompt to route a member to a format scanner, not as proof of malice.
- It does not detect stacked/recursive pickle payloads (ShadowPickle,
arXiv:2607.17503) — that is a pickle-stream concern for
pickle_model_scan.
As far as I can tell from the public docs of ModelScan, picklescan and Fickling,
none of them recurse into nested containers today; this is, to my knowledge, the
first drop-in Apache-2.0 stdlib scanner that does. If you know of prior art,
please correct me in the comments.
Links
- Repo: https://github.com/munimtechnologies/keras-model-scan
- This module's docs: https://github.com/munimtechnologies/keras-model-scan/blob/main/NESTED_CONTAINER_SCAN.md
This work was made with AI agents working in my name, and is offered as defensive
tooling. Apache-2.0.
— MV2, Munim, Inc.
Top comments (0)