[Bug]: HTML injection in the extra-networks cards via model filename (script runs in the WebUI origin on page load)
Checklist
- The issue exists after disabling all extensions
- The issue exists on a clean installation of webui
- The issue is caused by an extension, but I believe it is caused by a bug in the webui
- The issue exists in the current version of the webui
- The issue has not been reported before recently
- The issue has been reported before but has not been fixed yet
What happened?
A model filename is interpolated into the extra-networks card HTML without escaping, so a filename containing " and markup is parsed as HTML in the WebUI origin. The filename is attacker-chosen: it is simply the name of a file inside a model package (for example a LoRA) that the victim downloads and unpacks into the models tree. The file's contents are a perfectly valid safetensors file written by the official writer — only its name is the payload.
Injection point (modules/ui_extra_networks.py, line 273):
btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]})html/extra-networks-copy-path-button.html places that value inside a double-quoted attribute, data-clipboard-text="{filename}". str.format performs no escaping, so a " in the filename terminates the attribute and everything after it is parsed as markup. The finished card block is inserted with innerHTML in javascript/extraNetworks.js (line 662), so an onerror handler on an injected executes with the WebUI's origin.
The contrast with the same card shows this is an oversight, not a trade-off: sort_keys (line 306) and description (line 328) are both passed through html.escape; the filename is the one field that is not. The same string also reaches a second, independent unescaped sink: search_terms_from_path(filename) goes into the search_terms span interpolated at lines 312-320, likewise without html.escape.
Cards are built by interface.load (modules/ui_extra_networks.py:788) while the page loads, so the injected handler runs as soon as the victim opens the WebUI — no click on the extra-networks panel, no model load.
Verified on a clean install at commit 82a973c04367123ae98bd9abdf80d9eda9b910e2 with --skip-load-model-at-start (no checkpoint present): with a sample file whose 191-byte filename is
x"><img src=x onerror='var s=String.fromCharCode(47);fetch(s+"sdapi"+s+"v1"+s+"refresh-checkpoints",{method:"POST"}).then(r=>document.documentElement.dataset.mbe2e="W"+r.status)'>.safetensorsthe handler executed on page load and the product answered its same-origin POST /sdapi/v1/refresh-checkpoints with HTTP 200 (the marker string "W200" the handler writes onto document.documentElement was observed in the page). A control sample with a benign filename produced nothing. Both files are identical 152-byte safetensors files; only the name differs.
Steps to reproduce the problem
- On Linux or macOS, create a LoRA file whose name contains the markup above (Windows cannot: NTFS forbids
"in filenames; a path component is limited to 255 bytes, and the sample name is 191 bytes). The file content can be produced with the official writer: safetensors.torch.save_file({"lora_down.weight": torch.zeros(4)}, path, metadata={"format": "pt", "ss_output_name": "mbe2e"}). - Put the file in models/Lora of a clean webui install.
- Start the WebUI and open http://127.0.0.1:7860/ in a browser.
- The injected
handler runs while the page loads; no click is needed. For a harmless demo you can replace the fetch(...) call with alert(1) in the filename.
- Control: rename the same file to benign_lora.safetensors, restart, reload — nothing fires.
What should have happened?
The filename should be escaped (html.escape) wherever it is interpolated into the card HTML, the way sort_keys and description already are in the same function. A filename can never legitimately contain markup, so it should not be able to break out of the data-clipboard-text attribute or the search_terms span.
What browsers do you use to access the UI ?
Google Chrome
Sysinfo
Sysinfo of the verification instance is attached (sysinfo-redacted.json; personal paths and the hostname are redacted as user/HOST). The instance was installed by unpacking the source tarball of the pinned commit 82a973c04367123ae98bd9abdf80d9eda9b910e2 (not a git clone), so the Commit field inside the file reads ; the affected commit is the one stated in this report. Environment: WSL2 Ubuntu 24.04, Python 3.10.21, torch 2.1.2+cpu, no third-party extensions.
Console logs
Full console log of the verification instance (same pinned commit 82a973c04367123ae98bd9abdf80d9eda9b910e2) from startup to API readiness, for both the control sample (benign_lora.safetensors) and the payload-named sample; the complete log follows. Key lines:
Launching Web UI with arguments: -f --api --skip-prepare-environment --skip-torch-cuda-test --skip-version-check --no-half --use-cpu all --no-download-sd-model --skip-load-model-at-start --port 7860 --lora-dir ~/poc-work/artifact/neg
no module 'xformers'. Processing without...
Warning: caught exception 'Torch not compiled with CUDA enabled', memory monitor disabled
Running on local URL: http://127.0.0.1:7860
Startup time: 5.6s (import torch: 2.9s, import gradio: 0.6s, setup paths: 0.4s, initialize shared: 0.2s, other imports: 0.3s, load scripts: 0.5s, create ui: 0.3s, gradio launch: 0.3s, add APIs: 0.1s).
Neither phase shows any server-side anomaly: the injection happens in the browser, so the server log stays silent — which is exactly why this issue is easy to miss.Additional information
- Impact: script execution in the WebUI origin as soon as the page opens. The injected script runs with the user's session and can drive same-origin endpoints; in the documented run it issued POST /sdapi/v1/refresh-checkpoints, which the product accepted (HTTP 200). This is XSS in the browser (CWE-79); no OS-level code execution is claimed.
- Constraint: on Linux/macOS any byte except / and NUL is legal in a path component, so the payload travels as a filename inside an ordinary model archive shared on a model site. Windows hosts cannot carry it (NTFS forbids quotes), but the models directory is commonly populated from Unix-sourced archives.
- Suggested fix: wrap the value at modules/ui_extra_networks.py:273 in html.escape and escape the search_terms value at :312-320 (both precedents — sort_keys :306 and description :328 — are in the same function). A more durable change is to escape every untrusted field once, where the item dict is built.
Source: AUTOMATIC1111/stable-diffusion-webui