You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

PoC: fickling 0.1.12 HuggingFace scanner β€” pickle+EOCD polyglot routes malicious pickle to the zip-member scanner β†’ reported CLEAN while it executes

Target tool: trailofbits/fickling β€” pickle static analyzer / scanner Affected version tested: fickling 0.1.12 (latest on PyPI), Python 3.12, installed into a fresh venv Affected surface: fickling --huggingface REPO / fickling.cli._scan_huggingface (the Hub repo scanner), driven by fickling.polyglot.find_file_properties Vulnerability class: CWE-436 (Interpretation Conflict) / container-format-detection confusion β†’ scanner evasion β†’ arbitrary code execution on load Impact: A raw malicious pickle with a 22-byte empty-ZIP End-Of-Central-Directory record appended is reported by fickling's HF scanner as No obvious safety issues detected (EXIT_CLEAN, exit 0), while pickle.load / torch.load (legacy) still execute the attacker's os.system(...).

This is the SAME structural container-confusion class that defeats picklescan's zip EOCD trailer routing, generalized to a different scanner (fickling). It is distinct from the other filed fickling finding (the aliased _os.popen allowlist bypass of check_safety) β€” that bug is in pickle-opcode analysis; this one never reaches pickle analysis at all because the file is misrouted to the ZIP member scanner.


Root cause

fickling/polyglot.py:201 computes the routing property with the standard library helper:

is_standard_zip = zipfile.is_zipfile(file)   # True if an EOCD signature appears ANYWHERE in the file

zipfile.is_zipfile() scans for the 4-byte EOCD signature PK\x05\x06 anywhere in the file, so appending an empty-zip EOCD (22 bytes) to any file makes is_standard_zip == True, even though the file is a raw pickle with no ZIP local headers / central directory entries.

fickling/cli.py::_scan_huggingface then routes on that property, checking zip before pickle:

props = find_file_properties(local_path)
is_zip = props["is_torch_zip"] or props["is_standard_zip"] or ext in HF_ZIP_PICKLE_EXTENSIONS
if is_zip:
    member_results = scan_zip_archive(local_path, ...)   # <-- WINS for the polyglot
elif props["is_valid_pickle"] or ext in HF_RAW_PICKLE_EXTENSIONS:
    file_results = [scan_file(local_path, ...)]           # <-- the raw-pickle analyzer, never reached

scan_zip_archive (in fickling/loader.py) opens the file with RelaxedZipFile and only analyzes archive members whose extension is .pkl/.pickle/.bin. For the polyglot, the appended empty-EOCD central directory has zero members, so infolist() is empty β†’ nothing is scanned β†’ the scanner returns an empty result set β†’ overall_safe stays True.

The malicious pickle bytes sit in the file before the EOCD, are never inside a real ZIP member, and are therefore never handed to scan_file / check_safety.

Note: is_standard_zip is checked before is_valid_pickle. fickling's own identify_pytorch_file_format() checks is_valid_pickle first and correctly labels the same polyglot as legacy PyTorch/pickle β€” so the defect is specifically the routing precedence in _scan_huggingface combined with the EOCD-anywhere zipfile.is_zipfile.

Why the real loader still executes

The pickle machine stops at the STOP (.) opcode; any trailing bytes (the appended EOCD) are ignored. So:

  • pickle.load(open("mal_poly.pkl","rb")) β†’ executes os.system("touch /tmp/...").
  • For a legacy (non-zip) torch.save file + EOCD, torch.load(..., weights_only=False) takes its legacy pickle path (first 4 bytes are the pickle magic, not PK\x03\x04, so torch's own strict _is_zipfile is False) and executes the payload.

Verified differential (all on current released versions)

File Real loader fickling --hf modelscan 0.8.8 picklescan 1.0.5
mal.pkl (plain) pickle.load RCE UNSAFE (exit 1) CRITICAL INFECTED
mal_poly.pkl (pickle+EOCD) pickle.load RCE CLEAN (exit 0) ⚠ CRITICAL (robust) CLEAN (picklescan's own EOCD bug)
mal_torch.bin (legacy torch) torch.load RCE UNSAFE (exit 1) clean (unrelated legacy-multipickle miss) INFECTED
mal_torch_poly.bin (legacy+EOCD) torch.load RCE CLEAN (exit 0) ⚠ clean INFECTED (robust)

The two ⚠ rows are the bypass. Crucially the bypass defeats a scanner that the OTHER tools catch: on mal_poly.pkl, fickling passes it while modelscan flags it CRITICAL; on mal_torch_poly.bin, fickling passes it while picklescan flags it INFECTED. So this is a genuine fickling-specific defense-evasion primitive, not a shared blind spot.

modelscan is robust to this trailer trick (it routes by file extension and scans .pkl bytes directly; its PyTorch path uses a strict first-4-bytes _is_zipfile), so the honest scope of this finding is fickling's HuggingFace scanner, not modelscan.

Reproduce

python -m venv venv && . venv/bin/activate
pip install "fickling[huggingface]"==0.1.12   # pulls torch + huggingface_hub
python build_poc.py           # writes mal.pkl / mal_poly.pkl (pickle + empty-EOCD)
python build_torch_poly.py    # writes mal_torch.bin / mal_torch_poly.bin (legacy torch + EOCD)
python verify_fickling.py     # shows find_file_properties + routing verdict (CLEAN on polyglot)
python e2e_hf.py              # drives the SHIPPED fickling.cli._scan_huggingface end-to-end

e2e_hf.py monkeypatches only the network layer (HfApi.repo_info / hf_hub_download) to serve the local file; every routing/scanning decision is fickling's real released code. Observed output:

CONTROL (plain malicious legacy-torch pickle):  exit 1  "Potentially unsafe content detected!"
ATTACK  (same payload + empty-zip EOCD trailer): exit 0  "No obvious safety issues detected"

The os.system payload here only runs touch /tmp/FICKLING_*_BYPASS_PROOF β€” a benign marker. Run in a throwaway sandbox.

Remediation

  • In _scan_huggingface, check is_valid_pickle before the zip branch, or additionally run scan_file whenever is_valid_pickle is true even if is_standard_zip is true (a file can be both).
  • Do not use zipfile.is_zipfile() (EOCD-anywhere) as a container discriminator for untrusted input; require the ZIP local-file-header magic at offset 0 (as torch's strict _is_zipfile does), and/or reject files that are simultaneously a valid pickle and claim to be a zip.
  • When scan_zip_archive finds zero analyzable members in a file that also parses as a pickle, treat it as SUSPICIOUS rather than clean.

Dup-check

Distinct from: picklescan's zipfile.is_zipfile EOCD trailer bypass (different tool); the aliased _os.popen / stdlib-reexport allowlist bypass of fickling.check_safety (different mechanism β€” opcode analysis, not container routing); and fickling's local --check-safety path (which uses StackedPickle.load directly and is NOT affected). This report is specifically the --huggingface scanner's is_standard_zip-before-is_valid_pickle routing precedence combined with EOCD-anywhere detection.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support