The Model Was Inert, the File Beside It Was Not: Two Stable Diffusion WebUI CVEs and the 94-Issue Sweep Behind Them
NVD published CVE-2026-111986 at 13:16:35.920Z and CVE-2026-111987 at 14:16:54.630Z on 18 October 2026. Both carry vulnStatus Received, both come from the VulDB CNA, and both are against AUTOMATIC1111/stable-diffusion-webui — at 165,073 stars and 33,618 forks, the most-starred Stable Diffusion interface on GitHub and the de facto reference implementation of “load a model someone else made.” Both CVE descriptions end with the same clause: “The project was informed of the problem early through an issue report but has not responded yet.”
That clause is doing more work than it appears to. We checked the repository directly. The master branch HEAD is commit 82a973c04367123ae98bd9abdf80d9eda9b910e2, committed 27 July 2024, and it is the same commit the v1.10.1 release tag points at. Both reports have been open since 30 September 2026 with zero comments, among 2,511 open issues. The project is not archived and not marked disabled, so no banner warns a downloader that nobody is home — and it ships no SECURITY.md, which means a public GitHub issue was the only disclosure channel the reporter had.
The more interesting finding is not either CVE. It is what these two are a sample of: the reporter, 3em0, has filed 94 issues across 77 distinct repositories since 1 September 2026 — 65 of them in October alone — and they are overwhelmingly one defect class, stated 94 different ways. The a1111 pair are simply the first two to be assigned numbers.
CVE-2026-111986: the YAML sitting next to the checkpoint is a constructor call
Stable Diffusion checkpoints are commonly distributed with a same-basename .yaml config beside the weights. WebUI adopts that file automatically. Read first-hand in modules/sd_models_config.py at the v1.10.1 release commit:
def find_checkpoint_config_near_filename(info):
if info is None:
return None
config = f"{os.path.splitext(info.filename)[0]}.yaml"
if os.path.exists(config):
return config
return None
There is no validation, no allow-list and no provenance check: if a file with the right name exists, it is the config. load_model then parses it with OmegaConf and hands sd_config.model straight to the instantiator (modules/sd_models.py, same commit):
sd_config = OmegaConf.load(checkpoint_config)
repair_config(sd_config, state_dict)
...
sd_model = instantiate_from_config(sd_config.model, state_dict)
And instantiate_from_config is a generic object factory over an attacker-supplied string:
def instantiate_from_config(config, state_dict=None):
constructor = get_obj_from_str(config["target"])
params = {**config.get("params", {})}
...
return constructor(**params)
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
...
return getattr(importlib.import_module(module, package=None), cls)
The YAML names any importable Python object and the keyword arguments to call it with. That is the whole bug: importlib.import_module on a module name taken from a downloaded file. The reporter’s gadget is torch.hub.load with source: local pointed at the package’s own directory, because torch.hub imports the target directory’s hubconf.py and runs its top-level code. Three ordinary files — model.safetensors, model.yaml, hubconf.py — execute arbitrary Python the moment the victim selects the checkpoint in the dropdown.
One detail in the report is the mark of someone who tested rather than theorised. WebUI mutates the config before instantiating it, in repair_config:
def repair_config(sd_config, state_dict=None):
if not hasattr(sd_config.model.params, "use_ema"):
sd_config.model.params.use_ema = False
An injected use_ema=False would be a TypeError for most constructors and would break the chain. It does not break this one, because torch.hub.load forwards unknown keywords through **kwargs. The reporter says so explicitly — the gadget “survives the real product path” — and backs it with a negative control: an identical package differing only in model.target: torch.nn.Identity loads the same sidecar, prints the same console line, and does not run hubconf.py. That control is what separates a demonstration from a claim, and it is why the finding is credible without anyone re-running it.
The severity numbers are where we part company with the record. VulDB scored it 2.1 under CVSS 4.0 and 6.3 under CVSS 3.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L) with CWE-74 and CWE-94. The reporter proposed 8.8 High (C:H/I:H/A:H, CWE-470). Partial confidentiality, integrity and availability impact is not a defensible reading of arbitrary Python executing in the WebUI process: that process holds the user’s outputs, their other models, their extension tree, and on the common single-user desktop install, their home directory. The 2.1 is the number a scanner will show you. It is not the number the demonstrated capability supports.
CVE-2026-111987: two fields on the card were escaped, the third was not
The second CVE is nominally stored XSS, and the nominal part is the problem. A LoRA’s safetensors metadata is copied verbatim out of the file header by read_metadata_from_safetensors. The Lora extension then adopts one of those strings as the model’s identity, in extensions-builtin/Lora/network.py:
self.set_hash(
self.metadata.get('sshs_model_hash') or
hashes.sha256_from_cache(self.filename, "lora/" + self.name, use_addnet_hash=self.is_safetensors) or
''
)
No length check, no character-set check, no format check — whatever the file says its hash is, that is the hash. The value is appended to the card’s search terms, and then rendered. The rendering code in modules/ui_extra_networks.py is worth reading as a single block, because the defect is visible as an asymmetry inside twenty lines:
sort_keys = " ".join(
[
f'data-sort-{k}="{html.escape(str(v))}"'
for k, v in item.get("sort_keys", {}).items()
]
).strip()
search_terms_html = ""
search_term_template = "<span class='hidden {class}'>{search_term}</span>"
for search_term in item.get("search_terms", []):
search_terms_html += search_term_template.format(...)
description = (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else "")
if not shared.opts.extra_networks_card_description_is_html:
description = html.escape(description)
sort_keys is escaped. description is escaped. search_term — sitting between them, interpolated into an HTML string literal — is not. Nobody decided that metadata should be markup; the field simply missed a convention its neighbours follow. And because card HTML for every registered page is built during interface.load, the payload is in the DOM when the page opens: no click, no model load, no checkpoint selected.
CVSS 3.1 4.3, CVE 4.0 2.1, CWE-79. Then read the title the reporter actually gave it: “Stored XSS via LoRA safetensors sshs_model_hash metadata, escalated to RCE through the extension installer.” The escalation chain is not speculation, and we verified every structural precondition at the same commit:
- The extension installer is driven by two hidden Gradio controls with fixed element IDs —
gr.Text(elem_id="extension_to_install", visible=False)andgr.Button(elem_id="install_extension_button", visible=False)— so same-origin JavaScript can fill and click them exactly as the product’s own front-end does. install_extension_from_urlcallsgit.Repo.clone_from(url, tmpdir), and a local directory is a valid git URL — so a model folder obtained bygit clonefrom a model host is itself clonable as an “extension.”run_extension_installerthen executes the package’sinstall.pyin a subprocess. Stage one.- The gate is
check_access(), governed bycmd_opts.disable_extension_access = cmd_opts.webui_is_non_local and not cmd_opts.enable_insecure_extension_access. On a default localhost launch,webui_is_non_localis false, so extension access is enabled. The reporter states this precisely: under--listenthe XSS still fires and only the escalation is blocked. - The script then presses “Apply and restart UI,” and the restarted process imports the installed extension’s
scripts/. Stage two, across the restart boundary — persistence.
A sub-finding buried in the reproduction deserves its own line: the milestone INSTALL_UNCONFIRMED. The install path clones the repository and runs install.py before the step that raises on the never-loaded extension index — so the Gradio call aborts, the UI displays nothing at all, and neither the clone nor the code execution is rolled back. The victim’s interface shows no error, no install, no trace. A failed operation that has already executed attacker code and left it on disk is worse than a successful one, because the only observable signal is silence.
CWE-79 at 4.3 describes the first link of that chain. It does not describe a model download that ends in a self-restarting process with an attacker-authored extension loaded at boot. We have made this observation before about guardrail CVEs scored against the changed code rather than the reached capability; the asymmetry reappears here with a different sign.
Safe tensors, unsafe package
Here is what makes this pair worth a briefing rather than a line in a feed. In both proofs of concept, the .safetensors file is valid, inert, and written with the official safetensors writer API. There is no pickle, no torch.load, no deserialisation gadget. The format did its job perfectly.
safetensors exists because the ML ecosystem concluded that model weights should not be a code-execution vector, and the migration away from pickle was a genuine security win — one we have covered repeatedly, from LMCache’s cache speaking pickle to SGLang’s malicious GGUF files. What these two CVEs show is where the execution went instead. It did not disappear; it moved one file over, into the YAML, the config.json, the adapter_config.json, the model_index.json, the weight_map — the unglamorous metadata nobody thought of as a trust boundary because it is “just configuration.”
A safetensors file is a safe file format. A model package is a directory, and the directory was never in scope. “We use safetensors” is a statement about one file in it. The unit of distribution is what users actually download, and nothing validates the unit.
The sweep: 94 issues, 77 repositories, one defect
We enumerated the reporter’s issue history through the GitHub API rather than inferring it. Since 1 September 2026: 94 issues across 77 distinct repositories, of which 85 remain open and 84 have zero comments. Sixty-five were filed in October alone. Four are against a1111 (#17497–#17500, all open, all unanswered); two of those four became today’s CVEs, and the other two — HTML injection via model filename, and an unchecked safetensors header length used directly in file.read() — have no identifiers yet.
Read the October titles in sequence and the class is unmistakable. Arbitrary module import from model_index.json in huggingface/finetrainers. Path traversal via weight_map values in tjake/Jlama, lxtGH/OMG-Seg, alibaba/Pai-Megatron-Patch, UbiquitousLearning/mllm and dexmal/dexbotic. An adapter’s base_model_name_or_path deciding which base model gets downloaded and served in lm-sys/FastChat, myshell-ai/AIlice, HazyResearch/minions and SakanaAI/text-to-lora. Hydra _target_ dispatch in espnet/espnet and HazyResearch/ThunderKittens. Unsafe reflection in braindecode, prophesier/diff-svc and pytorch/pytorch. Checkpoint-controlled indices reaching unbounded array reads in turboderp-org/exllamav3 and kakao/khaiii.
Every one is the same sentence with the nouns changed: a value inside a model package is used to decide what code runs or which file is opened. Whether it surfaces as reflection, traversal, an out-of-bounds read or an unsafe download is a function of the host language, not of the defect. That is a genuine structural finding about the ML ecosystem, and it is more useful than either CVE number.
It also has a cost, and the honest version of this story includes it. Eighty-four of 94 reports have drawn no reply. Some of that is dormant projects. Some of it is that a maintainer receiving an unsolicited security issue has no way to triage at this rate, and queue volume is itself a maintainer resource — a tension we documented when SGLang’s maintainer closed an entire account’s submission queue, and with it the one PR that fixed a 9.2. We will also note plainly that this author’s earlier 2026 activity includes a handful of issues titled aa, aaa and [BUG] against NVIDIA, Apache and other projects, which is exactly the sort of noise that trains maintainers to stop reading. The September–October reports are a different standard of work — source lines, negative controls, reproducible artefacts, proposed fixes, self-imposed scope limits — but they arrive through the same door, at a volume no volunteer queue absorbs. Reports this good deserve a better channel than the one that exists, and a dormant 165,000-star project is proof that the channel is where the system breaks.
What to do
There is no patched version of stable-diffusion-webui, and on current evidence there will not be one. Both CVEs are “up to 1.10.1,” 1.10.1 is the latest release, and master has not moved in over two years. Treat remediation as your problem, not the vendor’s:
- Treat a model package as an archive, not a file. Before loading anything downloaded, list the directory. A
.yamlbeside a checkpoint, ahubconf.py, aninstall.py, or a.gitdirectory inside a model folder are not normal model contents, and any one of them is sufficient on its own here. - Delete same-basename sidecar YAMLs you did not write. WebUI adopts them silently; removing the file is the complete mitigation for CVE-2026-111986 and requires no code change. Confirm which config is in effect — the product reports it in
GET /sdapi/v1/sd-modelsand printsCreating model from config: <path>at load time. - Do not
git clonemodel repositories intomodels/. Download the weights. The.gitdirectory is a precondition of the second-stage escalation, and a plain archive without it breaks that chain. - If you must run it, run it without extension access and without your home directory. Containerise, mount models read-only, and be aware that the usual advice to add
--listenis a trade, not a fix: it disables the escalation and leaves the XSS. - Check your forks. The vulnerable code is in widely-forked files; downstream UIs built on this tree inherit both defects unless they diverged on these specific functions. Grep your own deployment for
get_obj_from_str,find_checkpoint_config_near_filenameand the unescapedsearch_term_template. - Generalise past a1111. The two-line audit that finds this class anywhere: does any string from a downloaded package reach
importlib.import_module,getattr,eval,os.path.joinagainst a trusted root, or an outbound model fetch? If yes, that string is code or a path, and it came from the internet.
Neither CVE is in the CISA KEV catalogue (checked directly, 18 October 2026, catalogue version 2026.10.16, 1,748 entries; zero hits for either identifier and no stable-diffusion-webui records of any kind), and no exploitation has been reported. Both exploits are publicly available as attachments on the open issues, which is the state the CVE descriptions record with “The exploit has been publicly disclosed and may be utilized.”
Our verification was read-only. Both CVE identifiers, publication timestamps (13:16:35.920Z and 14:16:54.630Z), vulnStatus (Received), source identifier (cna@vuldb.com), descriptions, CVSS 4.0/3.1/2.0 vectors and scores, CWE assignments and reference URLs were read first-hand from the NVD 2.0 API. All quoted Python — find_checkpoint_config_near_filename, instantiate_from_config, get_obj_from_str, repair_config, the OmegaConf.load call site, read_metadata_from_safetensors, NetworkOnDisk.set_hash, the extra-networks card builder, the hidden installer controls, run_extension_installer, install_extension_from_url and the disable_extension_access computation — was fetched at release commit 82a973c04367123ae98bd9abdf80d9eda9b910e2 and quoted verbatim. Repository metadata (165,073 stars, 33,618 forks, 2,511 open issues, AGPL-3.0, not archived, not disabled), the identity of master HEAD with the v1.10.1 release commit, its 27 July 2024 commit date, the release tag list, and the absence of SECURITY.md were read first-hand from the GitHub API. Issue numbers, titles, authors, creation timestamps, open states and zero comment counts for #17497–#17500, and the full issue bodies quoted and summarised above, were read first-hand from the GitHub API. The 94-issue / 77-repository / 85-open / 84-zero-comment tally was computed from a complete enumeration of the author’s issue history via the GitHub search API and classified by reading each title. KEV status was checked directly against CISA’s catalogue. Brave Search was used for discovery and returned no independent technical analysis of either CVE — only automated mirrors (VulDB 421652 and 421653, Threat Radar, strix.ai, TheHackerWire) reprinting the NVD text. All reproduction results, timings, canary files and DOM milestones are the reporter’s measurements, not ours. We installed nothing, downloaded no proof-of-concept archive, built no model package, authored no metadata value, started no WebUI instance, and executed no proof of concept.
Sources:
- NVD — CVE-2026-111986 (AUTOMATIC1111 stable-diffusion-webui up to 1.10.1, code injection via get_obj_from_str in Model Instantiation; CWE-74/CWE-94; CVSS 4.0 2.1, CVSS 3.1 6.3; published 18 October 2026)
- NVD — CVE-2026-111987 (AUTOMATIC1111 stable-diffusion-webui up to 1.10.1, XSS via __metadata__.sshs_model_hash in the Extra-Networks Card Builder; CWE-79/CWE-94; CVSS 4.0 2.1, CVSS 3.1 4.3; published 18 October 2026)
- stable-diffusion-webui issue #17499 — “Arbitrary code execution through same-basename YAML sidecar of a checkpoint (model.target)” (filed by 3em0, 30 September 2026; open, zero comments)
- stable-diffusion-webui issue #17500 — “Stored XSS via LoRA safetensors sshs_model_hash metadata, escalated to RCE through the extension installer” (filed by 3em0, 30 September 2026; open, zero comments)
- stable-diffusion-webui — modules/sd_models.py at the v1.10.1 release commit (instantiate_from_config, get_obj_from_str, repair_config, read_metadata_from_safetensors)
- stable-diffusion-webui — modules/ui_extra_networks.py at the v1.10.1 release commit (escaped sort_keys and description, unescaped search_term_template)
- stable-diffusion-webui — modules/shared_cmd_options.py (disable_extension_access is false on a default localhost launch)
- stable-diffusion-webui release v1.10.1 — the latest release, and the same commit as master HEAD
- CISA — Known Exploited Vulnerabilities catalogue (neither CVE listed; checked 18 October 2026, version 2026.10.16)
- al-ice.ai — “Looking at a Model Ran Its Code: Unsloth Studio’s Picker Was Arbitrary Code Execution, and No CVE Was Issued” (4 October 2026; the same class, no identifier)
- al-ice.ai — “The Sandbox Was an Empty Dict: SGLang’s Dumper eval() RCE Ships in 0.5.22 After Its Fix Was Closed as Queue Spam” (16 October 2026; report volume versus maintainer capacity)
- al-ice.ai — “SGLang CVE-2026-5760 — RCE via Malicious GGUF Model Files” (20 April 2026; execution inside the weight file, the problem safetensors solved)
- al-ice.ai — “vLLM CVE-2026-4944 — Hardcoded trust_remote_code Bypass Enables RCE” (29 May 2026; the model package deciding what the loader trusts)
- al-ice.ai — “The Mirror Is the Attacker: Two of 25 Package Managers Survive a Malicious Repository” (18 October 2026; the unit of distribution as the trust boundary)