Key Takeaway
CVE-2026-4372 affects Hugging Face transformers before 5.3.0: a crafted config.json can point an internal attention field at an attacker-controlled Hub repository, and from_pretrained then downloads and runs its Python code even though trust_remote_code is off. Upgrade to 5.3.0 or later, pin model revisions to reviewed commit hashes, and load from pre-downloaded snapshots.
The sentence in the CVE-2026-4372 advisory that stopped me is the one saying the flaw is reached through standard, documented usage. Not a hand-built exploit chain, not an exotic flag: an ordinary from_pretrained call on a model repository, with trust_remote_code left at its default of False. Most inference hosts I would audit rely on exactly that default as their only gate.
This post reads the primary sources closely: the GitLab and GitHub advisories, the fix commit a7f8e7f, and the current Hugging Face documentation on remote code, kernels, offline mode and pickle scanning. It does not include an exploit, and it separates what the advisory states from what the diff shows and from what I could not confirm. The aim is a loading checklist you can apply to a self-hosted inference host today.
What the advisory actually says
The GitLab advisory, published 26 May 2026 and updated 1 July 2026, titles the issue a remote code execution in HuggingFace transformers. It says a malicious config.json can set the _attn_implementation_internal field to an attacker-controlled Hub repository ID. When a victim loads that model with AutoModelForCausalLM.from_pretrained(), the library downloads and runs Python code from that repository with the victim's operating system privileges. The advisory attributes this to unfiltered deserialization of config attributes, weak sanitization of internal fields and unsandboxed execution of downloaded kernels.
Three facts decide your exposure. Every version before 5.3.0 is affected, and 5.3.0 is the only fixed version the advisory lists. The CVSS 3.0 base score is 7.8 (High), vector AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, with CWE-502 and CWE-1066 assigned. And the advisory says the flaw bypasses the trust_remote_code mechanism and occurs during standard usage, so the person loading the model may not notice anything. The GitHub Advisory Database entry GHSA-29pf-2h5f-8g72 agrees on versions and score, credits an analyst, links a huntr bounty report, and lists an EPSS of 0.599 percent when I read it.
Why trust_remote_code was the wrong place to look
A review that greps for trust_remote_code=True and stops there treats one flag as the whole boundary. The documentation shows at least three separate ways a model load can run code, each with its own gate or none at all. The table lists them, and the last row is the one this CVE added to the picture.
| Path | What triggers it | What gates it | What the gate does not cover |
|---|---|---|---|
| Custom modelling code | Loading a repo that ships its own modelling files | trust_remote_code, default False; the docs say to set it only for repositories you trust and whose code you have read | Nothing else: it guards only this path |
| Hub attention kernels | An attn_implementation string shaped like org/repo | Current docs: kernels-community is trusted, any other repo needs allow_all_kernels=True | Only helps if the code enforcing it runs on the value that reached it |
| Pickled weights | Loading a pickle-format checkpoint such as pytorch_model.bin | No flag. The Hub shows pickle imports per file and runs ClamAV scans | The Hub's own docs call the scan not 100 percent foolproof |
| config.json internal field (CVE-2026-4372) | A Hub config carrying the _attn_implementation_internal key | Per the advisory, trust_remote_code did not gate it in versions before 5.3.0 | Everything: the value arrived as data, not as a flag you passed |
The lesson is about where untrusted data becomes a code path. A flag you pass yourself is easy to audit. A string that arrives inside a downloaded file and is treated as configuration is not, which is why the fix had to change how the config is read rather than add another warning.
What the fix commit changes
The commit is a7f8e7f, titled only fix security vuln, touching two files for 22 added and 9 removed lines. Its message names no CVE, so everything below comes from the diff and its code comments, not from a write-up. Two guards were added, and the second is the more informative.
# Condensed from the diff of commit a7f8e7f. Not a verbatim patch.
# src/transformers/configuration_utils.py, in the loop that applies extra
# kwargs from a Hub config as attributes on the config object.
# Wrong (before): every key from the downloaded config became an attribute.
setattr(self, key, value)
# Right (after): the two internal fields are skipped. The diff comment says
# these should use the public field instead.
if key not in ("_attn_implementation_internal", "_experts_implementation_internal"):
setattr(self, key, value) # still inside the existing try/except
# src/transformers/integrations/hub_kernels.py, is_kernel(attn_implementation)
# Before: one regex; any string shaped like org/repo[@rev][:name] was a Hub kernel.
# After: None returns False; the org name is captured; anything outside
# "kernels-community" logs warning_once ("For security reasons, we
# currently only accept kernels from the kernels-community ...")
# and returns False.The first guard makes configuration loading ignore the two internal fields, so a Hub config can no longer assign them. A comment in the diff says those fields should use the public field instead. The second guard narrows what counts as a Hub kernel: the org name is now captured, and anything outside kernels-community logs a warning and returns False, with a comment giving the reason as loading random kernels leading to arbitrary code execution. Together they cut the path at its source and again at its sink.
Do not read this as proof that nothing else is reachable. The advisory attributes the flaw to three causes, and the commit addresses the two I can see in the diff. I did not audit 5.3.0 beyond that commit, and the advisory lists no patched release on the 4.x line, so a host pinned to 4.x has no fix to take other than the major upgrade.
Check whether a host is exposed
Two things are worth checking on every machine that loads models: the installed library version, and any cached config that carries the internal key with a repository-shaped value. The script below uses only the standard library. It reads the cache location the way the Hugging Face documentation describes, HF_HUB_CACHE first, then HF_HOME with a hub subfolder, and it does not try to reproduce anything harmful.
#!/usr/bin/env python3
"""Audit one host for CVE-2026-4372 exposure. Read-only; standard library only.
Exit code 1 means: upgrade, or review a flagged cache entry."""
import json
import os
import re
import sys
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
FIXED = (5, 3, 0) # the advisories list 5.3.0 as the fixed version
INTERNAL_KEYS = {"_attn_implementation_internal", "_experts_implementation_internal"}
TRUSTED_ORG = "kernels-community"
def parse(v: str) -> tuple:
# "5.3.0rc1" -> (5, 3, 0). re.match avoids reading "0rc1" as 1.
return tuple(int(re.match(r"\d+", p).group()) if re.match(r"\d+", p) else 0
for p in v.split(".")[:3])
def cache_root() -> Path:
# Order from the huggingface_hub docs: HF_HUB_CACHE, else HF_HOME/hub.
# XDG_CACHE_HOME only matters when HF_HOME is unset and is not handled here.
if os.environ.get("HF_HUB_CACHE"):
return Path(os.environ["HF_HUB_CACHE"])
home = os.environ.get("HF_HOME") or str(Path.home() / ".cache" / "huggingface")
return Path(home) / "hub"
def internal_values(node, path=""):
if isinstance(node, dict):
for key, val in node.items():
if key in INTERNAL_KEYS:
yield f"{path}/{key}", val
yield from internal_values(val, f"{path}/{key}")
elif isinstance(node, list):
for i, val in enumerate(node):
yield from internal_values(val, f"{path}[{i}]")
def main() -> int:
bad = 0
try:
installed = version("transformers")
status = "OK" if parse(installed) >= FIXED else "VULNERABLE"
bad += status == "VULNERABLE"
print(f"transformers {installed}: {status} (fixed in 5.3.0)")
except PackageNotFoundError:
print("transformers not installed in this interpreter")
for cfg in cache_root().rglob("config.json"):
try:
data = json.loads(cfg.read_text())
except (OSError, ValueError):
continue
for where, val in internal_values(data):
# A repo-shaped value outside the trusted org is a lead, not proof.
if isinstance(val, str) and "/" in val and val.split("/")[0] != TRUSTED_ORG:
bad += 1
print(f"REVIEW {cfg} {where} = {val}")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())Treat a hit as a lead for review rather than proof of compromise. I could not confirm from the sources whether a legitimate saved config ever contains these keys, so the script only flags a repo-shaped value from outside kernels-community. If it flags something, find which model repository that cache entry came from and who published the commit. This script also does not see XDG_CACHE_HOME, which the docs say applies only when HF_HOME is unset.
The failure mode: auditing the flag instead of the data
The named failure here is a review that passes because every call site is clean. Every from_pretrained in the codebase omits trust_remote_code, a grep proves it, and the host is signed off as safe. Under the advisory that grep is not enough, because the dangerous input was a field inside a file fetched at load time.
There is a second, quieter trade-off in pinning. The documentation says the revision argument accepts a branch, a tag or a commit id. A branch or tag can move, so a config.json that was clean at review can be replaced later, while a commit hash cannot. The installation docs add that with a commit hash Transformers uses the local cache for that commit's files, whereas a branch or tag costs one Hub lookup at load. Pinning to a hash trades convenience for a config you can reason about.
Upgrading carries its own cost. The only fixed release in the advisory is 5.3.0, so a team on an older major version takes a major upgrade to close this. I did not verify what else changed between those lines, so run your model-load tests on a staging host first and do not assume the jump is free.
A loading checklist for self-hosted inference
The pattern I would use splits loading into an online build step and an offline runtime. In the build step, an authenticated CI job downloads a snapshot at a commit hash a human reviewed. At runtime the container has HF_HUB_OFFLINE=1, which the docs say prevents HTTP calls to the Hub, plus local_files_only so the load fails loudly if anything is missing instead of reaching out.
# --- build stage (CI, online): download once, at a commit a human reviewed ---
from huggingface_hub import snapshot_download
REVIEWED = {
# model repo -> full commit hash recorded in version control, never "main"
"meta-llama/Llama-3.2-1B": "PASTE_THE_REVIEWED_40_CHAR_COMMIT_HASH",
}
for repo_id, commit in REVIEWED.items():
snapshot_download(repo_id=repo_id, revision=commit)
# --- runtime stage (container, offline) ---
# Dockerfile, not Python: huggingface_hub reads its variables at import time,
# so os.environ["HF_HUB_OFFLINE"] set after the import does nothing.
# ENV HF_HUB_OFFLINE=1 USE_HUB_KERNELS=0 HF_HOME=/models/hf
from transformers import AutoModelForCausalLM
MODEL_ID = "meta-llama/Llama-3.2-1B"
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
revision=REVIEWED[MODEL_ID], # a hash cannot move; a branch or tag can
local_files_only=True, # fail loudly instead of reaching out
attn_implementation="sdpa", # built-in PyTorch attention, no Hub kernel
# trust_remote_code stays at its default False. Do not add allow_all_kernels.
)Two details matter. The docs state that huggingface_hub reads its environment variables at import time, so setting HF_HUB_OFFLINE inside Python after the import does nothing; put it in the container environment. And the offline docs note that a branch or tag can still resolve through the cache, which is one more reason to pin a hash. I did not confirm whether offline mode also blocks the kernels library from fetching a kernel, so the checklist also disables Hub kernels by setting USE_HUB_KERNELS=0, which the kernels guide says disables them everywhere without changing code.
Add a CI check that fails the build on a literal trust_remote_code=True or allow_all_kernels=True anywhere in your repository. It will not catch a value arriving through data, but it stops the two documented opt-ins from being added quietly in a config template.
What each control stops, and what it leaves open
No single control closes every path, and some of them overlap. This table is my own reading of the documentation, with the limits stated, so you can decide which ones to adopt and which gaps to accept.
| Control | What it stops | What it leaves open | Based on |
|---|---|---|---|
| Upgrade to 5.3.0 or later | The config.json path described in CVE-2026-4372 | Pickled weights and any path not yet found | Advisory |
| Pin revision to a reviewed commit hash | A repository changing its config after you reviewed it | A commit you reviewed carelessly | Transformers auto classes and installation docs |
| Pre-download, then HF_HUB_OFFLINE=1 and local_files_only | Surprise fetches from the Hub at startup | A bad artefact baked into the image. Kernel fetching is unconfirmed | Installation and huggingface_hub docs |
| USE_HUB_KERNELS=0 | Hub kernels everywhere, per the kernels guide | Anything that is not a kernel, and the speed those kernels gave you | Loading kernels guide |
| Never pass allow_all_kernels or trust_remote_code as True | The two documented opt-ins to third-party code | Data that reaches the code path without an opt-in, which is this CVE | Loading kernels guide and auto classes docs |
| Trusted publishers and signed commits for weights | Casual use of unknown pickle checkpoints | A signature proves origin, not that the file is safe | Hub pickle scanning doc |
One extra control comes from me rather than the sources: restrict outbound network access from the inference host to the artefact store you actually use. Code downloaded at load time has to come from somewhere, and an egress allow-list turns a silent fetch into a blocked and logged one. I have not tested this against this CVE, so count it as defence in depth, not a fix.
What to do on Monday
Work through these in order. The first two take minutes and tell you whether the rest is urgent.
- Run the audit script on every host that loads models, including laptops that build images, and record the library version it reports.
- Upgrade anything below 5.3.0 on a staging host, run your model-load and generation tests, then promote. Add a version floor to your constraints file so a later resolve cannot go backwards.
- Replace branch and tag references in your loaders with reviewed commit hashes, and keep the hashes in version control next to the code.
- Move downloading into CI, bake the snapshot into the image or volume, and set HF_HUB_OFFLINE=1, USE_HUB_KERNELS=0 and local_files_only in the runtime.
- Add the CI grep for the two opt-ins, and decide who owns reviewing a model repository before its hash is allowed into the list.
# 1. What is installed, and is it below 5.3.0?
python -m pip show transformers | grep -i '^version'
python audit_transformers_cve.py ; echo "exit=$?"
# 2. Hold the floor so a later resolve cannot go backwards.
echo 'transformers>=5.3.0' >> constraints.txt
python -m pip install -c constraints.txt -U transformers
# 3. CI guard: fail the build on the two documented opt-ins to third-party code.
if grep -rnE 'trust_remote_code *= *True|allow_all_kernels *= *True' src/ ; then
echo "third-party code opt-in found" >&2 ; exit 1
fiIf the audit finds a suspicious cached config, treat the host as potentially affected: identify the source repository, rotate any credentials that host could read, and check what the process was allowed to reach. That is general incident practice, not something the advisory prescribes.
The rule I take from this is that a security flag only protects the paths it was written for. Upgrade to 5.3.0 or later, load pinned snapshots from an offline runtime, and audit the data a loader consumes as carefully as the arguments you pass it.
Sources and further reading
- GitLab Advisory Database: CVE-2026-4372 in transformers
- GitHub Advisory Database: GHSA-29pf-2h5f-8g72
- huggingface/transformers commit a7f8e7f: the security fix diff
- Hugging Face Transformers docs: loading kernels and allow_all_kernels
- Hugging Face Transformers docs: auto classes, trust_remote_code and revision
- Hugging Face Transformers docs: installation, cache and offline mode
- huggingface_hub docs: environment variables, including HF_HUB_OFFLINE
- Hugging Face Hub docs: pickle scanning

