CodeVetter · Repo Unpack · Risk map
AUTOMATIC1111/stable-diffusion-webui
The dominant risk theme is remote-code-execution surface by design: extensions are arbitrary Python loaded at startup, the Extensions tab can git-clone and run install.py, and the Custom code script exec()s user code — mitigated only by flags that auto-disable extension access when the server is non-local. The API is unauthenticated unless --api-auth is set and can fetch remote URLs (SSRF-checked but opt-out-able). Shell-out is pervasive in launch_utils (shell=True subprocess). Checkpoint loading relies on a monkeypatched torch.load pickle scan that --disable-safe-unpickle bypasses.
- Arbitrary code execution by design: every .py in extensions/*/scripts and extensions/*/preload.py is exec_module'd at startup, and install_extension_from_url runs the extension's install.py; protection is only cmd_opts.disable_extension_access = webui_is_non_local AND not --enable-insecure-extension-access, asserted by check_access().
- scripts/custom_code.py executes arbitrary Python via exec()/eval() (exec_with_return builds an AST and runs it) gated only by --allow-code flag and a re-check inside run().
- The REST API has no auth by default: --api-auth is optional HTTP Basic; without it every /sdapi/v1/* route (including POST /options which can change output dirs, /train/* jobs, and --api-server-stop's /server-kill|restart|stop) is open to anyone who can reach the port.
- SSRF surface: decode_base64_to_image fetches http(s) URLs server-side when opts.api_enable_requests; verify_url() checks resolved IPs are global (blocks local addresses only when api_forbid_local_requests), but DNS-rebinding between gethostbyname_ex and requests.get is not closed.
- shell=True subprocess everywhere in launch_utils.run(): pip installs, git clone/pull/fetch/reset built as f-strings; git_clone interpolates URL/dir into a shell string (quoted but not escaped); TORCH_COMMAND/INDEX_URL env vars directly shape pip commands.
- Checkpoint deserialization: torch.load is globally replaced by safe.load which scans pickles (check_pt) for malicious code before unsafe_torch_load; --disable-safe-unpickle turns this off entirely; the scan is a blocklist-style heuristic, and safetensors path bypasses it.
- Gradio's permissive CORS middleware is manually stripped post-launch (app.user_middleware filter) specifically because a malicious page could otherwise drive the local API to install extensions; any code re-adding middleware could silently reopen this hole.
- File-system writes outside repo root: --data-dir/--ckpt-dir/--embeddings-dir and outdir_* options let users/API redirect outputs and model loads to arbitrary paths; images.save_image and Options.save have no sandboxing beyond --gradio-allowed-path for serving and --hide-ui-dir-config for hiding dir settings.
- Single global queue_lock + mutable shared.sd_model/shared.opts: process_images mutates global opts via override_settings then restores in finally — an exception or crash between set and restore, or concurrent API calls interleaving opts changes, can leave settings or the loaded checkpoint in an inconsistent state. (inferred)
- Options.__setattr__ uses assert for freeze-settings enforcement ('assert not cmd_opts.freeze_settings'); running Python with -O would strip these checks, silently disabling the freeze protections.
- Startup update path fetches remote state and prints notices (version_check hits api.github.com; --update-all-extensions runs git pull --autostash across every repo under extensions_dir including subrepos) — network-dependent and can leave extension trees in autostash-conflict states.
- sys.path is temporarily prepended with each script's basedir during load_scripts() — an extension script can shadow stdlib/modules imports for subsequently loaded scripts (restored in finally per file, but cross-file ordering via metadata.ini Requires/Before/After is attacker-orderable). (inferred)