mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-26 10:07:38 +08:00
feat(security): dedicated install flags decouple git_url/pip from security_level
Install via git URL and pip install are no longer gated by
security_level. Each surface gets a dedicated config.ini flag —
allow_git_url_install / allow_pip_install (both default false, secure
by default) — that fully REPLACES the security-level term for these two
features. The network-position invariant is retained: a non-local
listener stays denied regardless of the flags unless
network_mode = personal_cloud.
- New pure predicate is_dedicated_install_allowed() in
common/manager_security (no config access; callers resolve config)
- Legacy endpoints /v2/customnode/install/git_url and .../pip switch
from is_allowed_security_level('high+') to the flag gate; batch
installs of unknown git URLs likewise (middle+ entry gate unchanged,
unknown-pip 'block' stays unconditional; response shapes preserved)
- Config readers/writers (glob + legacy) parse and persist the flags;
denial logs and frontend 403 messages name the responsible flag and
note the non-local-listener requirement (network_mode=personal_cloud)
- No auto-seed from security_level — users previously on weak/normal-
must opt in explicitly (see CHANGELOG migration notes; README
documents the new contract)
- Update the pre-existing permissive E2E harness
(start_comfyui_permissive.sh + test_e2e_legacy_real_ops.py) to the
new contract: it now also sets allow_git_url_install /
allow_pip_install = true, since security_level = normal- alone no
longer opens the git_url/pip endpoints
Tests: predicate truth table proving security_level independence in
both directions, dual-reader config contract, security-level-matrix
freeze guards, legacy gate regression guards (121 unit), plus 22
real-server E2E tests incl. URL-form pip install with self-clean.
This commit is contained in:
@@ -83,6 +83,25 @@ def is_loopback(address):
|
||||
return False
|
||||
|
||||
|
||||
def is_dedicated_install_allowed(flag_value: bool, listen_address: str, network_mode: str) -> bool:
|
||||
"""P-direct predicate for the dedicated install flags (goal265-spec.md §1.2).
|
||||
|
||||
allowed iff flag AND (loopback listener OR network_mode == 'personal_cloud').
|
||||
|
||||
Gates the git-URL / standalone-pip install surfaces via the dedicated
|
||||
``allow_git_url_install`` / ``allow_pip_install`` config flags, fully
|
||||
decoupled from ``security_level`` (REPLACE, not AND — spec §1.1 inv. 1).
|
||||
The network-position term retains today's invariant that a public
|
||||
(non-loopback, non-personal_cloud) listener stays denied regardless of
|
||||
the flags (spec §1.1 inv. 2).
|
||||
|
||||
Pure function — NO config access; callers resolve ``flag_value`` and
|
||||
``network_mode`` through their own config reader and pass values in
|
||||
(preserves common/ layering: this module must stay config-import-free).
|
||||
"""
|
||||
return bool(flag_value) and (is_loopback(listen_address) or network_mode.lower() == 'personal_cloud')
|
||||
|
||||
|
||||
def do_nothing():
|
||||
pass
|
||||
|
||||
|
||||
@@ -1706,6 +1706,8 @@ def write_config():
|
||||
'network_mode': get_config()['network_mode'],
|
||||
'db_mode': get_config()['db_mode'],
|
||||
'verbose': get_config()['verbose'],
|
||||
'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
|
||||
@@ -1755,6 +1757,8 @@ def read_config():
|
||||
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
|
||||
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
|
||||
'verbose': get_bool('verbose', False),
|
||||
'allow_git_url_install': get_bool('allow_git_url_install', False),
|
||||
'allow_pip_install': get_bool('allow_pip_install', False),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
@@ -1783,6 +1787,8 @@ def read_config():
|
||||
'security_level': SecurityLevel.NORMAL.value,
|
||||
'db_mode': DBMode.CACHE.value,
|
||||
'verbose': False,
|
||||
'allow_git_url_install': False,
|
||||
'allow_pip_install': False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ export async function install_pip(packages) {
|
||||
});
|
||||
|
||||
if(res.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
show_message("To use this feature, set <code>allow_pip_install = true</code> in the [default] section of config.ini. This setting is independent of security_level.<BR>Note: if the ComfyUI listener is not local, <code>network_mode = personal_cloud</code> is also required.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ export async function install_via_git_url(url, manager_dialog) {
|
||||
});
|
||||
|
||||
if(res.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
show_message("To use this feature, set <code>allow_git_url_install = true</code> in the [default] section of config.ini. This setting is independent of security_level.<BR>Note: if the ComfyUI listener is not local, <code>network_mode = personal_cloud</code> is also required.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1690,6 +1690,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
|
||||
@@ -1734,6 +1736,8 @@ def read_config():
|
||||
'network_mode': default_conf.get('network_mode', NetworkMode.PUBLIC.value).lower(),
|
||||
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
|
||||
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
|
||||
'allow_git_url_install': get_bool('allow_git_url_install', False),
|
||||
'allow_pip_install': get_bool('allow_pip_install', False),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
@@ -1757,6 +1761,8 @@ def read_config():
|
||||
'network_mode': NetworkMode.PUBLIC.value,
|
||||
'security_level': SecurityLevel.NORMAL.value,
|
||||
'db_mode': DBMode.CACHE.value,
|
||||
'allow_git_url_install': False,
|
||||
'allow_pip_install': False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ SECURITY_MESSAGE_HIGH_P = "ERROR: To use this action, '--listen' must be set to
|
||||
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/Comfy-Org/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/Comfy-Org/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. Please contact the administrator.\nReference: https://github.com/Comfy-Org/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. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
|
||||
routes = PromptServer.instance.routes
|
||||
|
||||
@@ -122,6 +124,18 @@ def is_allowed_security_level(level):
|
||||
return True
|
||||
|
||||
|
||||
def _dedicated_install_allowed(flag_key: str) -> bool:
|
||||
"""goal265: P-direct gate for the dedicated install flags (spec §1.2).
|
||||
|
||||
allowed iff config[flag_key] AND (loopback listener OR
|
||||
network_mode == 'personal_cloud') — fully decoupled from security_level.
|
||||
Resolves config through the LEGACY reader and delegates the pure
|
||||
predicate to common/manager_security (which stays config-import-free).
|
||||
"""
|
||||
return manager_security.is_dedicated_install_allowed(
|
||||
core.get_config()[flag_key], args.listen, core.get_config()['network_mode'])
|
||||
|
||||
|
||||
async def get_risky_level(files, pip_packages):
|
||||
json_data1 = await core.get_data_by_mode('local', 'custom-node-list.json')
|
||||
json_data2 = await core.get_data_by_mode('cache', 'custom-node-list.json', channel_url='https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main')
|
||||
@@ -1473,7 +1487,15 @@ async def _install_custom_node(json_data):
|
||||
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):
|
||||
# goal265 S-C (middle+ entry gate above UNCHANGED): unknown git URL ('high+')
|
||||
# -> dedicated-flag full predicate replaces the security_level check (spec §1.2);
|
||||
# unknown pip ('block') -> unconditional deny via is_allowed_security_level (Q1).
|
||||
# Flag-deny PRESERVES today's 404 response shape at this position (R1).
|
||||
if risky_level == 'high+':
|
||||
if not _dedicated_install_allowed('allow_git_url_install'):
|
||||
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):
|
||||
logging.error(SECURITY_MESSAGE_GENERAL)
|
||||
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
@@ -1527,8 +1549,9 @@ async def _fix_custom_node(json_data):
|
||||
|
||||
@routes.post("/v2/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)
|
||||
# goal265 S-A: dedicated-flag gate, decoupled from security_level (spec §1.2).
|
||||
if not _dedicated_install_allowed('allow_git_url_install'):
|
||||
logging.error(SECURITY_MESSAGE_FLAG_GIT_URL)
|
||||
return web.Response(status=403)
|
||||
|
||||
url = await request.text()
|
||||
@@ -1547,8 +1570,9 @@ async def install_custom_node_git_url(request):
|
||||
|
||||
@routes.post("/v2/customnode/install/pip")
|
||||
async def install_custom_node_pip(request):
|
||||
if not is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
||||
# goal265 S-B: dedicated-flag gate, decoupled from security_level (spec §1.2).
|
||||
if not _dedicated_install_allowed('allow_pip_install'):
|
||||
logging.error(SECURITY_MESSAGE_FLAG_PIP)
|
||||
return web.Response(status=403)
|
||||
|
||||
packages = await request.text()
|
||||
|
||||
Reference in New Issue
Block a user