Back to all posts
Published on · by Renaud Deraison

The flag said no

On August 3, 2026, The Hacker News covered FaceHugger: three flaws in Hugging Face's Diffusers library — seven million downloads a month — that let a crafted model repository execute arbitrary code on the machine that loads it, even with trust_remote_code=False set. The root cause: the trust check ran at a different point from the code load. Coding agents pull models the way they pull packages, so the flag that lied was guarding developer laptops. Bromure Agentic Coding puts the boundary under the process instead of inside it: the load runs in a disposable VM, the credentials in reach are decoys, and the payload's call home dies at the host proxy — so the malicious repo costs the attacker their payload and costs you nothing.

The repository contained a file named None.py, and that was the whole trick. The safety flag was set, the check ran, and the code ran anyway, because the check and the load happened at two different moments — and the attacker only had to be present at the second one.

On August 3, 2026, The Hacker News picked up research that Zafran Labs published in late July: three vulnerabilities in Hugging Face's Diffusers library, collectively named FaceHugger, that let a crafted model repository execute arbitrary Python code on any machine that loads it. Diffusers is the standard way to run diffusion models — about seven million downloads a month, sitting in production pipelines, CI jobs, notebooks, and container images. The library has a safeguard for exactly this scenario: trust_remote_code, a flag that is supposed to stop unreviewed code from a model repo from running on your machine. All three flaws walk past it while it is set to False.

The check and the load were two different events

Loading a model from the Hub is not one operation. The library makes two sequential HTTP requests: the first fetches the repository's configuration, the second fetches the code and weights the configuration points to. The trust_remote_code gate did its inspection during the first request. The thing it was guarding against arrived in the second.

Zafran's researchers found three ways through that gap, now CVE-tagged and patched in Diffusers 0.38.0. In CVE-2026-44827 (CVSS 8.8), a string-formatting quirk builds the filename None.py when no custom pipeline is requested — and the existence check for that file runs on a code path that doesn't flag it, so a repository that ships a file named None.py passes the gate and executes on load. CVE-2026-45804 is a race: about a third of a second separates the two requests, and a repository that changes between them is validated as one thing and executed as another. CVE-2026-44513 skips the gate on local snapshot loads. Zafran's researchers compressed all three into one sentence: any method that makes the loader see custom code that the gate did not, allows bypassing.

The pattern has a history. Transformers — the other pillar of the Hugging Face ecosystem — had its own trust_remote_code=False bypass (CVE-2026-4372) exploitable through a standard model-loading call. The bug class comes with the design: a security promise living inside the process it polices. The flag is an argument to a function, while the enforcement is scattered across code paths, filename formatting, and request timing. When any of those disagree with the flag, the flag loses, and nothing tells you it lost. The model loads and the demo works.

Your agent loads models on a Tuesday

Read that story from a coding agent's chair. "Pull the inpainting pipeline off the Hub and benchmark it against ours" is an ordinary Tuesday task. The project pins diffusers==0.37, because projects pin things. The agent writes what a careful engineer would write — from_pretrained(..., trust_remote_code=False) — and the careful version is the vulnerable version. You can read a package's source before installing it; a model repo is config, weights, and, thanks to the loader, whatever the second HTTP request says it is.

Agents have made that load path hot. They fetch models the way they fetch packages, mid-task and by name, and attackers have already worked out that the recommendation itself can be poisoned. A model repo that detonates on load is the same play one registry over, with a better disguise: the payload sits in an artifact you file under data, not code.

Zafran's advice is to treat model repositories as untrusted code. Correct — and it has a prerequisite the report doesn't dwell on: a place where untrusted code can run without costing you anything. A policy of suspicion is a discipline you maintain until a deadline gets loud. A room built for detonation doesn't need your discipline.

The load happens in the box

Bromure Agentic Coding is that room, and it starts where FaceHugger's flag failed: the boundary sits underneath the process, in an environment the loader's internals can't argue with, enforced whether or not the process cooperates.

Walk the attack through a profile. Your agent takes the benchmark task, pins and all, and calls the loader on the malicious repo. None.py executes — inside a disposable Linux VM, one hypervisor away from macOS. The machine the payload now owns is a machine that exists for this task, holds this task's workspace, and gets discarded when the work is merged.

The payload's first real move is outbound. It has to be: fetch a second stage, or ship what it found to its operator. Each of those requests crosses the host proxy, where the destination is checked against the list you approved for this profile. huggingface.co is on that list — that's how the model arrived. The attacker's drop box is not. The wire refuses, the way the gate should have — except this refusal doesn't depend on the library's code paths agreeing with each other. The proxy doesn't know or care which Python file is making the request. It checks the destination, finds nothing approved, and drops the connection.

Suppose the payload skips the network and goes shopping instead, through env vars and key files. It finds broker decoys. The real values live on the host and are injected at the wire, only for approved destinations — the passwords were never in the VM to steal. And if it tries to make its stay permanent — rewrite a config, switch something off — destructive shapes pause for a guardrail dialog on the host, and persistence in a machine with a lifespan of one task is a losing trade anyway.

Meanwhile the task succeeds. The benchmark runs, the numbers come back, you read the diff. The attacker spent a supply-chain position on a payload that executed in a room with nothing to steal and no way to call home. You spent nothing — not even attention, unless you enjoy reading proxy logs for sport.

Inside the process — where the flag livedRequest 1 · fetch configtrust_remote_code gate runs hereverdict: "no custom code" ✓~0.3 s — nothing re-checksRequest 2 · fetch codeNone.py arrives — and executesthe gate never sees it (CVE-2026-44827)The promise and the enforcement were two different moments in the same process.Under the process — where a Bromure profile checksDisposable Linux VMagent · loader · None.py — all of it, flag or no flagcredentials in reach: broker decoysdiscarded when the task endsevery requestHost proxyhuggingface.co → allowedattacker drop box → refusedreal keys injected here, at the wire
Where the check ran, and where it should have. Diffusers' trust gate inspected the configuration in the first HTTP request; the code arrived — and executed — in the second. A Bromure profile doesn't referee the loader's internals: the whole process runs inside a disposable VM, and every outbound request, whoever makes it, is checked at the host proxy against the destinations you approved.

Patch the library, keep the room

The fix note in Zafran's report says organizations should upgrade and then go hunting: applications, notebooks, inference services, and container images that pin older Diffusers releases. That hunt has no finish line — somewhere in your dependency tree, some project pins the version with the gap, and the next FaceHugger will be a different library's flag with a different bypass. The guard and the shell disagreed was this failure in a command allowlist; the sandbox was a sentence was this failure in a prompt. In each case a check lived inside the thing it was checking, and the thing won.

A profile's boundary doesn't share that fate, because it doesn't participate in the process it contains. The VM doesn't parse pipeline configs. The proxy doesn't have a None.py code path. Their one job — this machine is disposable, this wire refuses unlisted destinations — is the same job on the day a bypass is published as on the day it's patched. A Sumo Logic CISO, commenting on this research with no Bromure in mind, named the defenses that hold: "the unglamorous ones: egress control, segmentation and credential hygiene." We built the unglamorous ones into a profile you click.

Keep pulling models — that is where the leverage is, and your agent should be trying them for you. Stop letting a function argument be the only thing between the Hub and your laptop. Install Bromure Agentic Coding, give the agent a profile whose rules hold at the wire, and the next time a loader's promise breaks — and one will — the news story stays someone else's.