feat(security): dedicated install flags decouple git_url/pip from security_level

Add two boolean config.ini [default] flags — allow_git_url_install and
allow_pip_install (both default false) — that fully REPLACE the
security_level term on the legacy install surfaces:

- POST /v2/customnode/install/git_url (S-A) and
  POST /v2/customnode/install/pip (S-B) are now gated solely by their
  dedicated flag AND the retained network-position invariant
  (loopback listener OR network_mode=personal_cloud). security_level
  no longer affects these two surfaces in either direction.
- The batch unknown-URL branch (S-C) routes through the same predicate;
  the unknown-pip branch stays unconditionally blocked; the general
  middle+ batch entry gate is unchanged.
- New pure predicate is_dedicated_install_allowed() in
  common/manager_security.py (config-import-free; callers pass values
  from their own reader). Both config readers (glob + legacy) register
  the keys in read/write/fallback paths.
- Denial logs and frontend copy name the responsible flag instead of
  the misleading security_level guidance. Public listeners remain
  denied regardless of the flags (no exposure widening).
- README security policy updated: config keys documented, git-url/pip
  removed from the security_level risky table, and a dedicated-flags
  subsection (REPLACE semantics, network rule, batch behavior,
  restart-only activation, weak/normal- opt-in migration note).
- Migration: existing weak/normal- users must opt in via the new flags
  (CHANGELOG note; deliberate no auto-seed).

Includes the unit/config/guard test suites (88 tests): predicate truth
table, dual-reader config contract (missing/malformed keys read false,
round-trip, cache staleness), security_level-matrix freeze guards, and
suite-order-independent test stubs.
This commit is contained in:
Dr.Lt.Data
2026-06-08 02:12:14 +09:00
parent 01799f8cac
commit 16ba874566
12 changed files with 1076 additions and 8 deletions
@@ -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
+2
View File
@@ -5,6 +5,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/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. 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"
def is_loopback(address):
+6
View File
@@ -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,
}
+2 -2
View File
@@ -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.");
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.");
return;
}
+6
View File
@@ -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,
}
+29 -5
View File
@@ -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()