Heads up
Your SBOM may gain components. Five serialization extensions that were previously skipped are now scanned, and files carrying no recognized extension are now opened when their bytes are a pickle. A repo containing either will produce more components than before and may newly exit 2.
Nothing that was already scanned changes verdict: the old-vs-new differential across the bypass corpus and 134 fixture verdicts is empty in both modes.
Security — a payload can no longer hide behind its filename
Discovery decided what to open from a file's suffix alone. That is the whole of CVE-2025-1889: a pickle named config.p — or weights.dat, or a file with no extension at all — was never opened, so the scan reported No AI models found and exited 0. A clean bill of health on a directory carrying a reverse shell.
Files that nothing claims by name are now identified by content.
Adding .p to the extension list was deliberately rejected. The technique is any unexpected suffix — the attacker picks the name — so that would have flipped our published scorecard case while protecting nobody.
Two details worth stating, because they are where this kind of check usually goes wrong:
- It validates the pickle stack, not just the opcode syntax.
.is the STOP opcode, so a weaker "does it reach STOP" rule treats every stylesheet opening with a class selector as a one-opcode pickle. Measured against a realnode_modulesof 5,123 files, that rule claimed 11: seven JavaScript files, a TypeScript declaration, a stylesheet and two man pages. Those shapes are now regression tests. - Validation runs on the prefix ending at the first STOP, not the whole buffer — so a payload followed by a corrupt tail (the nullifAI shape) is still caught rather than thrown out along with its own garbage.
Reads are bounded: 64KB per unclaimed file, escalating only for a stream that parses cleanly and has not yet ended, capped at the same budget the inspector itself uses. A walk over that tree takes 1.52s and claims nothing.
Known limit, stated rather than hidden: a pickle whose first opcode carries an argument larger than that entire budget is not discovered by content.
Bypass scorecard: 8/11 → 9/11
cve-2025-1889-nonstandard-extension moves from missed to detected in both scan modes, and the regression floor is raised to lock it in. Run it yourself:
aisbom bypass-scorecardThe two remaining OrderedDict checkpoint by static analysis.
New formats scanned
- joblib, dill, NumPy object arrays and bare pickles —
.pkl,.pickle,.joblib,.dill,.npy,.npz. Every one is a pickle stream underneath, carrying exactly the arbitrary-code-execution risk of a.pt..pklis the sharpest case: the README has always documentedaisbom scan model.pkl --strict, and that command scanned nothing. - joblib's compression is opened, whichever codec it chose — zlib, gzip, bz2, lzma/xz and the legacy
ZFcontainer, all via the standard library.lz4andzstdare named but not opened (MEDIUM (Unscanned Container: lz4)), on the same reasoning as 7z. No runtime dependency is added: joblib, dill and numpy are not imported by the scanner. - dill's code-reconstruction globals are flagged —
_create_function,_create_code,_import_module,_get_attrare CRITICAL. dill's type and array helpers are deliberately not flagged, so a.dillholding only data still scans clean.
Pickle detection
- A raw array block no longer ends the scan. joblib splices raw buffers mid-pickle, which stopped disassembly ~226 bytes into a 552-byte file while the payload sat at byte 516. Unreachable bytes now get a second pass that recovers globals directly.
- A
.npyheader no longer decides whether to look.descris attacker-supplied, so a pickle behind a header claiming'<f8'would have been a one-line evasion. STACK_GLOBALoperands arriving via the pickle memo resolve correctly — reading only literals resolvednumpy.dtypeasdtype.dtype, a strict-mode false positive on every ordinary joblib model..npzmembers are read even when the archive fights back — a tampered CRC or header name goes through the same raw local-header read the PyTorch path uses.- No scan limit ends in a clean verdict. Every bound that leaves bytes unexamined now reports
MEDIUM (Pickle Scan Incomplete)rather than passing a prefix off as the whole file.
Fixed
hf://scans resolve the new formats. The resolver's extension list is now derived from the scanner's own dispatch sets rather than restated, so the two cannot drift again.- The decompression budget follows the read budget. It came from a default argument, which binds once at import — so a remote scan that fetched 2MB could expand 16MB.
What's not changing
Exit codes, output formats, and the CycloneDX/SPDX schemas are identical to v1.3.1. No new runtime dependencies.