diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..60090316 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog + +## Unreleased + +### Security policy: dedicated install flags (`allow_git_url_install` / `allow_pip_install`) + +Two new boolean keys in `config.ini` (`[default]` section), both defaulting to +`false`, now govern the arbitrary-install surfaces: + +| Flag | Governs | +|------|---------| +| `allow_git_url_install` | `POST /customnode/install/git_url` and the unknown-git-URL arm of `POST /manager/queue/install` (incl. reinstall delegation) — the entire install transaction, transitive dependency pip installs included. On the batch queue path the flag applies **in addition to** the queue's `security_level` entry gate (see below) | +| `allow_pip_install` | `POST /customnode/install/pip` only | + +These surfaces additionally require a **loopback listener** (`--listen` on a +loopback IP such as `127.0.0.1` or `::1` — not a general LAN/private address); +the flags never open a non-loopback deployment. On the two +**direct** endpoints (`POST /customnode/install/git_url` and +`POST /customnode/install/pip`), the flags fully **decouple** the surface +from `security_level`: it no longer has any effect in either direction — a +strict level cannot deny them when the flag is `true`, and a weak level +cannot allow them when the flag is `false`. On the **batch queue path** +(`POST /manager/queue/install`), the flag is **necessary but not +sufficient**: it gates the unknown-git-URL arm at the risky position, while +the queue's normal `security_level` entry gate (`middle`) remains in force — +at `security_level = strong`, batch unknown-URL installs stay denied even +with the flag set to `true`. `security_level` continues to govern every +other gated endpoint unchanged. Only the case-insensitive string `true` +enables a flag; a missing or malformed key reads as `false`. + +#### Migration note (no auto-seed) + +There is **no automatic migration** from `security_level`. Users who +previously relied on `security_level = weak` (or `normal-`) to use +install-via-git-URL / install-pip must now **opt in explicitly** by adding to +`config.ini`: + +```ini +[default] +allow_git_url_install = true +allow_pip_install = true +``` + +Changes take effect after a **restart** (no hot reload). + +#### Residual-risk note — outdated ComfyUI behavior change + +On outdated ComfyUI versions (no system-user API), the manager previously +forced `security_level = strong`, which unconditionally denied the +git-URL/pip install surfaces. After this change those surfaces are governed +by the new flags instead: an operator who explicitly sets a flag to `true` +on a **loopback** listener can now perform installs on outdated ComfyUI +where the forced-strong policy previously denied them. This is an accepted, +deliberate trade-off: it requires explicit operator opt-in, remains bounded +to loopback listeners, and the flag-deny path on outdated ComfyUI still +surfaces the `comfyui_outdated` notice. If you operate an outdated ComfyUI +deployment, leave both flags at their default `false` and update ComfyUI. diff --git a/README.md b/README.md index ea4919a7..7d2d01b9 100644 --- a/README.md +++ b/README.md @@ -384,19 +384,79 @@ When you run the `scan.sh` script: * all feature is available * `high` level risky features - * `Install via git url`, `pip install` - * Installation of custom nodes registered not in the `default channel`. - * Fix custom nodes + * Downloading models that are not in `.safetensors` format and not + registered in the `default channel` model list + * NOTE: `Install via git url`, `pip install`, and installation of custom nodes + not registered in the `default channel` are **no longer governed by + `security_level`** — they are governed by the dedicated install flags + described below. * `middle` level risky features * Uninstall/Update * Installation of custom nodes registered in the `default channel`. + * Fix custom nodes * Restore/Remove Snapshot * Restart * `low` level risky features * Update ComfyUI +### Dedicated install flags: `allow_git_url_install` / `allow_pip_install` + +The two arbitrary-install surfaces are governed by dedicated boolean keys in +`config.ini` (`[default]` section), fully **decoupled** from `security_level`: + + * `allow_git_url_install` + * governs `Install via Git URL` (`POST /customnode/install/git_url`) **and** + the unknown-git-URL arm of the batch install queue + (`POST /manager/queue/install`, including reinstall delegation) — i.e. + installing any custom node from a git URL that is not registered in the + `default channel` catalog + * on the **batch queue path**, the flag is **necessary but not + sufficient**: the queue's normal `security_level` entry gate (`middle`) + must ALSO pass — at `security_level = strong`, batch unknown-URL + installs stay denied even with the flag set to `true` (only the direct + `Install via Git URL` endpoint is fully independent of `security_level`) + * covers the **entire install transaction** it starts, including the + pack's transitive dependency pip installs + * `allow_pip_install` + * governs **only** the standalone `pip install` feature + (`POST /customnode/install/pip`) + +Key properties: + + * **Decoupled from `security_level` (replace, not and)** — on the two + **direct endpoints** (`Install via Git URL` and `pip install`), + `security_level` no longer has any effect in either direction: a strict + level cannot deny them when the flag is `true`, and a weak level cannot + allow them when the flag is `false`. (The batch queue path keeps its + `security_level` entry gate in ADDITION to the flag — see the scope bullet + above.) Every other gated feature remains governed by `security_level` as + described above. + * **Loopback only** — the flags take effect **only** when the server listens + on a loopback address (e.g. `--listen 127.0.0.1`). On a non-loopback + listener these surfaces stay denied regardless of the flags; the flags + never widen the exposure of a public deployment. + * **Default deny / explicit opt-in** — both flags default to `false`. Only + the case-insensitive string `true` enables a flag; a missing or malformed + key reads as `false`. + +To opt in, edit `config.ini`: + +```ini +[default] +allow_git_url_install = true +allow_pip_install = true +``` + +Changes take effect after a **restart** (no hot reload). + +> **Migration note**: there is no automatic migration from `security_level`. +> If you previously relied on `security_level = weak` (or `normal-`) to use +> install-via-git-URL / pip install, you must opt in explicitly with the flags +> above. See `CHANGELOG.md` for details, including a behavior note for +> outdated ComfyUI deployments. + # Disclaimer diff --git a/glob/manager_core.py b/glob/manager_core.py index 7c7e2fae..63ce1e4b 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -44,7 +44,7 @@ import manager_migration from node_package import InstalledNodePackage -version_code = [3, 40] +version_code = [3, 41] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') @@ -1699,6 +1699,8 @@ def write_config(): 'always_lazy_install': get_config()['always_lazy_install'], 'network_mode': get_config()['network_mode'], 'db_mode': get_config()['db_mode'], + 'allow_git_url_install': get_config()['allow_git_url_install'], + 'allow_pip_install': get_config()['allow_pip_install'], } # Sanitize all string values to prevent CRLF injection attacks @@ -1745,6 +1747,8 @@ def read_config(): 'network_mode': default_conf.get('network_mode', 'public').lower(), 'security_level': default_conf.get('security_level', 'normal').lower(), 'db_mode': default_conf.get('db_mode', 'cache').lower(), + 'allow_git_url_install': get_bool('allow_git_url_install', False), + 'allow_pip_install': get_bool('allow_pip_install', False), } manager_migration.force_security_level_if_needed(result) return result @@ -1774,6 +1778,8 @@ def read_config(): 'network_mode': 'public', # public | private | offline 'security_level': 'normal', # strong | normal | normal- | weak 'db_mode': 'cache', # local | cache | remote + 'allow_git_url_install': False, + 'allow_pip_install': False, } manager_migration.force_security_level_if_needed(result) return result diff --git a/glob/manager_server.py b/glob/manager_server.py index 8474bb9c..069b0d8a 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -35,6 +35,8 @@ SECURITY_MESSAGE_MIDDLE_OR_BELOW = "ERROR: To use this action, a security_level SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower." +SECURITY_MESSAGE_FLAG_GIT_URL = "ERROR: This action requires 'allow_git_url_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" +SECURITY_MESSAGE_FLAG_PIP = "ERROR: This action requires 'allow_pip_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" routes = PromptServer.instance.routes @@ -82,6 +84,19 @@ def is_loopback(address): except ValueError: return False + +def is_dedicated_install_allowed(flag_value: bool, listen_address: str) -> bool: + """P-direct predicate (adopter-degraded form): flag AND loopback. + + Pure helper for the dedicated install flags + (allow_git_url_install / allow_pip_install) — callers pass the + flag value from their own config read and the listener address + from the CLI arguments (request-time evaluation; the import-time + snapshot above is NOT consulted). + """ + return bool(flag_value) and is_loopback(listen_address) + + is_local_mode = is_loopback(args.listen) @@ -305,10 +320,18 @@ import zipfile import urllib.request -def security_403_response(): - """Return appropriate 403 response based on ComfyUI version.""" +def security_403_response(flag_token=None): + """Return appropriate 403 response based on ComfyUI version. + + When `flag_token` is given (dedicated install flag denials), the + body names the responsible flag instead of "security_level". The + `comfyui_outdated` branch stays the FIRST check regardless, and + no-arg callers keep today's body byte-identical. + """ if not manager_migration.has_system_user_api(): return web.json_response({"error": "comfyui_outdated"}, status=403) + if flag_token is not None: + return web.json_response({"error": flag_token}, status=403) return web.json_response({"error": "security_level"}, status=403) @@ -1384,7 +1407,17 @@ async def install_custom_node(request): else: return web.Response(status=404, text=f"Following node pack doesn't provide `nightly` version: ${git_url}") - if not is_allowed_security_level(risky_level): + if risky_level == 'high': + # unknown-URL arm: governed by the dedicated flag predicate + # (flag AND loopback, evaluated at request time). The loopback + # term is load-bearing here — the 'middle' entry gate above has + # no network-position term. + if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen): + logging.error(SECURITY_MESSAGE_FLAG_GIT_URL) + return web.Response(status=404, text="A security error has occurred. Please check the terminal logs") + elif not is_allowed_security_level(risky_level): + # 'block' arm stays an unconditional deny (is_allowed_security_level + # returns False for 'block'); 'middle'/'low' arms unchanged. logging.error(SECURITY_MESSAGE_GENERAL) return web.Response(status=404, text="A security error has occurred. Please check the terminal logs") @@ -1441,11 +1474,21 @@ async def fix_custom_node(request): @routes.post("/customnode/install/git_url") async def install_custom_node_git_url(request): - if not is_allowed_security_level('high'): - logging.error(SECURITY_MESSAGE_NORMAL_MINUS) - return security_403_response() + if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen): + logging.error(SECURITY_MESSAGE_FLAG_GIT_URL) + return security_403_response(flag_token='allow_git_url_install') - url = await request.text() + # Read the body as JSON (not raw text): a cross-origin