CVE-2026-9856: Verified Reproduction
CVE-2026-9856: huggingface/transformers <5.10.0: path traversal via chat template dict keys in save pretrained → arbitrary file write → RCE cron.d drop
CVE-2026-9856 is verified against the affected target. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00321.
What Is CVE-2026-9856?
CVE-2026-9856 is a high-severity RCE vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00321).
CVE-2026-9856 Severity
CVE-2026-9856 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce CVE-2026-9856
pruva-verify REPRO-2026-00321 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00321/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-9856
- reached the target end-to-end
- full exploit chain demonstrated
- high confidence
- the upstream fix blocks the same trigger
tokenizer_config.json chat_template dict key '../../../etc/cron.d/hf_pwn' and cron command content
- AutoTokenizer.from_pretrained(attacker_model).save_pretrained(out)
- save_chat_templates()
- /etc/cron.d/hf_pwn.jinja
- pre-existing cronie reload
- crond-spawned /bin/sh
How the agent worked
Root Cause and Exploit Chain for CVE-2026-9856
huggingface/transformers <5.10.0 contains a path traversal (CWE-22) in
save_pretrained(). When a tokenizer (or processor) is loaded from an
attacker-controlled artifact whose tokenizer_config.json carries a
chat_template dictionary, the dictionary keys are used verbatim as
filenames by save_chat_templates()
(src/transformers/tokenization_utils_base.py) and by
ProcessorMixin.save_pretrained() (src/transformers/processing_utils.py):
template_filepath = os.path.join(chat_template_dir, f"{template_name}.jinja")
with no sanitization. A traversal key such as ../../../etc/cron.d/hf_pwn
escapes the save directory and writes fully attacker-controlled content (with a
forced .jinja suffix) anywhere the victim process can write. This run
demonstrates the full impact chain: the escaped file is dropped into
/etc/cron.d, where a pre-existing, genuine cronie daemon naturally loads it
and independently spawns /bin/sh running the attacker's command — arbitrary
code execution with the victim's privileges.
- Package: huggingface/transformers (
save_pretrainedofPreTrainedTokenizerBaseandProcessorMixin). - Affected versions:
>=4.52.0, <5.10.0(verified vulnerable:5.9.0; NVD's<=5.8.0.dev0understates the range). - Fixed versions:
5.10.0(yanked ~20 min after release) /5.10.1+. - Risk: High. A victim application that loads an attacker-controlled model
(
from_pretrained) and later callssave_pretrained()— automatic in Trainer checkpointing, model conversion/re-hosting, and fine-tune export — writes attacker-controlled content to arbitrary filesystem paths. Realistic execution targets include/etc/cron.d(cron ignores the.jinjaextension) and overwriting existing.jinjatemplates rendered by a service. CVSS UI:R; the attacker needs no authentication.
Impact Parity
- Disclosed/claimed maximum impact: arbitrary file write → remote/code
execution (e.g., cron.d drop),
code_execution. - Reproduced impact from this run:
code_execution— two fresh, isolated victim processes running realtransformers==5.9.0each produced a unique marker file written by a/bin/shprocess that the genuine cronie 1.7.2 daemon independently spawned from the attacker-controlled/etc/cron.d/hf_pwn.jinja. The identical artifact againsttransformers==5.10.1raisedValueError, wrote no cron file, and produced no marker. - Parity:
full. - Not demonstrated: nothing — the claimed code-execution impact was reproduced end to end through the real library API and a real system daemon.
Root Cause
save_chat_templates() iterates tokenizer.chat_template.items() when the
chat template is a dict and computes
template_filepath = os.path.join(chat_template_dir, f"{template_name}.jinja").
template_name comes verbatim from tokenizer_config.json, which is fully
attacker-controlled when the model artifact is untrusted. No normalization or
containment check is applied, so keys containing .. (or absolute paths)
escape OUT/additional_chat_templates/. The library only mkdirs its own
additional_chat_templates dir, so the write succeeds whenever the
attacker-chosen parent directory (e.g. /etc/cron.d) already exists and is
writable by the victim. The identical flaw exists in
ProcessorMixin.save_pretrained() (processing_utils.py).
Fix: PR #46191, merge commit
eaaaf8494dd5386634ae37d1d122212fdc315be5 (2026-05-25), first shipped in
5.10.0/5.10.1. The fix resolves template_filepath's parent and compares
it to the resolved chat_template_dir, raising ValueError on mismatch (a
3-line guard in both files), plus regression tests using a ../../PWNED key.
Reproduction Steps
bundle/repro/reproduction_steps.sh(self-contained; helper:bundle/repro/hf_cron_exploit.py).- The script:
- Anchors source identity to the fixed commit
eaaaf8494dd5386634ae37d1d122212fdc315be5: its parent (47949d3a0e1cf9248f2a3eb3cd0deb12ee37b9e9) lacks the guard, the fix commit contains it (logs/source_identity.log). - Builds two Docker images from the immutable
fedora:42digest with cronie 1.7.2 and the exact PyPI releasestransformers==5.9.0(vulnerable) andtransformers==5.10.1(fixed). - In each isolated container: starts a real
crondbefore the attacker input, builds a tokenizer whosetokenizer_config.jsoncontainschat_template = {"../../../etc/cron.d/hf_pwn": "* * * * * root /bin/sh -c \"echo '<unique-marker>' > /proof/<unique-marker>.txt\""}, then calls the real APIAutoTokenizer.from_pretrained(dir).save_pretrained(out). - Waits for cronie to naturally load
/etc/cron.d/hf_pwn.jinjaand spawn the payload; the script never executes the dropped file. - Runs two fresh vulnerable victims and one fixed negative control.
- Anchors source identity to the fixed commit
- Expected evidence:
SAVE_RESULT=RETURNED,CRON_FILE_PRESENT=true,CRON_CONTENT_MATCH=true, cronie's ownlog_it ... CMD (/bin/sh -c ...)line, and a unique marker file per vulnerable attempt;SAVE_RESULT=BLOCKED/CRON_FILE_PRESENT=false/ no marker for the fixed control.
Evidence
bundle/logs/reproduction_steps.log— full driver log (two consecutive full passes succeeded; markers...-1651then...-2249).bundle/logs/source_identity.log— fix-commit anchor and guard diff.bundle/logs/vulnerable_cron_attempt1.log,bundle/logs/vulnerable_cron_attempt2.log—SAVE_RESULT=RETURNED,CRON_FILE_PRESENT=true,CRON_CONTENT_MATCH=true,MARKER_PRESENT=true, and cronie lines such aslog_it: (root 92) CMD (/bin/sh -c "echo 'CVE-2026-9856-RCE-VULNERABLE-1-2249' > /proof/...").bundle/logs/fixed_cron_control.log—SAVE_RESULT=BLOCKED,CRON_FILE_PRESENT=false,MARKER_PRESENT=false(ValueError guard).bundle/repro/marker_run1.txt,bundle/repro/marker_run2.txt— unique per-attempt markers written by the crond-spawned shell.bundle/repro/negative_control_fixed.json— strict negative-control observation (target_path_reached=true,marker_present=false).bundle/repro/runtime_manifest.json— entrypointfunction_call, target identity, and artifact digests.- Environment: Docker 27.5.1, immutable base
fedora@sha256:99e203b80b1c3d8f7e161ec10a68fd02b081ef83a3963553e513c82846b97814, cronie 1.7.2, Python 3.13,transformers==5.9.0/5.10.1, x86_64.
Recommendations / Next Steps
- Upgrade to
transformers>=5.10.1(5.10.0 was yanked). - The upstream guard (resolve the template path's parent and require it to
equal the resolved
chat_template_dir) is the correct containment fix; downstream backports should mirror it in bothtokenization_utils_base.pyandprocessing_utils.py. - Defensive controls: never call
save_pretrained()on artifacts loaded from untrusted sources without sandboxing; run converters/exporters with least privilege so/etc/cron.dand similar directories are not writable. - Detection: watch for non-system processes opening files under
/etc/cron.d, and for processes whose parent iscrondexecuting unexpected commands.
Additional Notes
- Idempotency: the script was executed twice consecutively in this run; both passes confirmed (exit 0), with fresh unique markers per pass. Images are rebuilt deterministically from the immutable base digest; per-attempt containers are removed after each case.
- The forced
.jinjasuffix does not prevent cron execution: cronie loads every file in/etc/cron.dregardless of extension. The write requires the victim to have write permission on the chosen parent directory (root in the demonstration container, matching the ticket's stated precondition). from_pretrained()alone is safe;save_pretrained()is the trigger.- This run independently revalidated the durable mechanics from prior
exploit-knowledge records (arbitrary write primitive
793c4388-299a-489f-a791-5e7b7f2e66d3, crond-mediated control flowffcce804-7fa3-4ec4-a772-357db3da686c, command-execution capability19959132-b2f0-4e9f-8e66-d7e6ab23edf5) with fresh current-run evidence.
CVE-2026-9856 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
cat /pruva/project-cache/.pruva/proof-carry/latest_confirmed/repro/validation_verdict.json 2>/dev/null; echo ---; cat /pruva/project-cache/.pruva/proof-carry/latest_confirmed/proof_carry_manifest.json 2>/dev/null | head -50