Compare commits

..

1 Commits

Author SHA1 Message Date
Dr.Lt.Data
e47d79e979
Merge 2866193baf into b9def4cb6e 2025-12-20 00:47:54 +03:00
10 changed files with 54 additions and 148 deletions

View File

@ -55,10 +55,9 @@ def should_be_disabled(fullpath:str) -> bool:
def get_client_ip(request):
peername = request.transport.get_extra_info("peername")
if peername is not None:
# Grab the first two values - there can be more, ie. with --listen
host, port = peername[:2]
host, port = peername
return host
return "unknown"

View File

@ -69,10 +69,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
form_factor = 'git-linux'
else:
form_factor = 'other'
from comfyui_manager.glob import manager_core
verbose = manager_core.get_config().get('verbose', False)
while remained:
# Add comfyui_version and form_factor to the API request
sub_uri = f'{base_url}/nodes?page={page}&limit=30&comfyui_version={comfyui_ver}&form_factor={form_factor}'
@ -82,7 +79,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
for x in sub_json_obj['nodes']:
full_nodes[x['id']] = x
if page % 5 == 0 and verbose:
if page % 5 == 0:
logging.info(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
page += 1

View File

@ -1,6 +1,4 @@
import os
from enum import Enum
from typing import Optional
is_personal_cloud_mode = False
handler_policy = {}
@ -36,35 +34,3 @@ def add_handler_policy(x, policy):
multiple_remote_alert = do_nothing
def is_safe_path_target(target: str) -> bool:
"""
Check if target string is safe from path traversal attacks.
Args:
target: User-provided filename or identifier
Returns:
True if safe, False if contains path traversal characters
"""
if '/' in target or '\\' in target or '..' in target or '\x00' in target:
return False
return True
def get_safe_file_path(target: str, base_dir: str, extension: str = ".json") -> Optional[str]:
"""
Safely construct a file path, preventing path traversal attacks.
Args:
target: User-provided filename (without extension)
base_dir: Base directory path
extension: File extension to append (default: ".json")
Returns:
Safe file path or None if input contains path traversal attempts
"""
if not is_safe_path_target(target):
return None
return os.path.join(base_dir, f"{target}{extension}")

View File

@ -41,15 +41,13 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
from ..common import context
version_code = [4, 0, 5]
version_code = [4, 0, 3]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
default_custom_nodes_path = None
@ -384,25 +382,16 @@ class UnifiedManager:
self.processed_install = set()
def get_module_name(self, x):
# 1. Direct cnr_id lookup
info = self.active_nodes.get(x)
if info is not None:
if info is None:
# Try to find in unknown_active_nodes by comparing normalized URLs
normalized_x = git_utils.normalize_url(x)
for url, fullpath in self.unknown_active_nodes.values():
if url is not None and git_utils.normalize_url(url) == normalized_x:
return os.path.basename(fullpath)
else:
return os.path.basename(info[1])
# 2. URL/aux_id → cnr_id conversion via repo_cnr_map
cnr_info = self.get_cnr_by_repo(x)
if cnr_info is not None:
cnr_id = cnr_info['id']
info = self.active_nodes.get(cnr_id)
if info is not None:
return os.path.basename(info[1])
# 3. Fallback: search unknown_active_nodes by URL
normalized_x = git_utils.normalize_url(x)
for url, fullpath in self.unknown_active_nodes.values():
if url is not None and git_utils.normalize_url(url) == normalized_x:
return os.path.basename(fullpath)
return None
def get_cnr_by_repo(self, url):
@ -1616,14 +1605,8 @@ def write_config():
'always_lazy_install': get_config()['always_lazy_install'],
'network_mode': get_config()['network_mode'],
'db_mode': get_config()['db_mode'],
'verbose': get_config()['verbose'],
}
# Sanitize all string values to prevent CRLF injection attacks
for key, value in config['default'].items():
if isinstance(value, str):
config['default'][key] = value.replace('\r', '').replace('\n', '').replace('\x00', '')
directory = os.path.dirname(context.manager_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
@ -1661,7 +1644,6 @@ 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(),
'verbose': get_bool('verbose', False),
}
except Exception:
@ -1687,7 +1669,6 @@ def read_config():
'network_mode': NetworkMode.PUBLIC.value,
'security_level': SecurityLevel.NORMAL.value,
'db_mode': DBMode.CACHE.value,
'verbose': False,
}
@ -2077,15 +2058,16 @@ class GitProgress(RemoteProgress):
def is_valid_url(url):
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if result.scheme and result.netloc:
return True
# Check for SSH git URL format
if SSH_URL_PATTERN.match(url):
return True
try:
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if all([result.scheme, result.netloc]):
return True
finally:
# Check for SSH git URL format
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
if pattern.match(url):
return True
return False

View File

@ -1294,17 +1294,11 @@ async def get_history(request):
try:
# Handle file-based batch history
if "id" in request.rel_url.query:
history_id = request.rel_url.query["id"]
# Prevent path traversal attacks
batch_path = security_utils.get_safe_file_path(history_id, context.manager_batch_history_path)
if batch_path is None:
logging.warning(f"[Security] Invalid history id rejected: {history_id}")
return web.Response(text="Invalid history id", status=400)
json_name = request.rel_url.query["id"] + ".json"
batch_path = os.path.join(context.manager_batch_history_path, json_name)
logging.debug(
"[ComfyUI-Manager] Fetching batch history: id=%s",
history_id,
request.rel_url.query["id"],
)
with open(batch_path, "r", encoding="utf-8") as file:
@ -1526,11 +1520,7 @@ async def remove_snapshot(request):
try:
target = request.rel_url.query["target"]
path = security_utils.get_safe_file_path(target, context.manager_snapshot_path)
if path is None:
logging.warning(f"[Security] Invalid snapshot target rejected: {target}")
return web.Response(text="Invalid target", status=400)
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
os.remove(path)
@ -1548,11 +1538,7 @@ async def restore_snapshot(request):
try:
target = request.rel_url.query["target"]
path = security_utils.get_safe_file_path(target, context.manager_snapshot_path)
if path is None:
logging.warning(f"[Security] Invalid snapshot target rejected: {target}")
return web.Response(text="Invalid target", status=400)
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
if not os.path.exists(context.manager_startup_script_path):
os.makedirs(context.manager_startup_script_path)

View File

@ -1,14 +1,14 @@
from comfyui_manager.glob import manager_core as core
from comfy.cli_args import args
from comfyui_manager.data_models import SecurityLevel, RiskLevel, ManagerDatabaseSource
from comfyui_manager.common.manager_security import (
is_loopback,
is_safe_path_target,
get_safe_file_path,
)
# Re-export for backward compatibility
__all__ = ['is_loopback', 'is_safe_path_target', 'get_safe_file_path', 'is_allowed_security_level', 'get_risky_level']
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
def is_allowed_security_level(level):

View File

@ -42,15 +42,13 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
from ..common import context
version_code = [4, 0, 5]
version_code = [4, 0, 3]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
default_custom_nodes_path = None
@ -1612,11 +1610,6 @@ def write_config():
'db_mode': get_config()['db_mode'],
}
# Sanitize all string values to prevent CRLF injection attacks
for key, value in config['default'].items():
if isinstance(value, str):
config['default'][key] = value.replace('\r', '').replace('\n', '').replace('\x00', '')
directory = os.path.dirname(context.manager_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
@ -2076,15 +2069,16 @@ class GitProgress(RemoteProgress):
def is_valid_url(url):
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if result.scheme and result.netloc:
return True
# Check for SSH git URL format
if SSH_URL_PATTERN.match(url):
return True
try:
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if all([result.scheme, result.netloc]):
return True
finally:
# Check for SSH git URL format
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
if pattern.match(url):
return True
return False

View File

@ -814,13 +814,8 @@ async def get_history_list(request):
@routes.get("/v2/manager/queue/history")
async def get_history(request):
try:
history_id = request.rel_url.query["id"]
# Prevent path traversal attacks
batch_path = manager_security.get_safe_file_path(history_id, context.manager_batch_history_path)
if batch_path is None:
logging.warning(f"[Security] Invalid history id rejected: {history_id}")
return web.Response(text="Invalid history id", status=400)
json_name = request.rel_url.query["id"]+'.json'
batch_path = os.path.join(context.manager_batch_history_path, json_name)
with open(batch_path, 'r', encoding='utf-8') as file:
json_str = file.read()
@ -1164,12 +1159,7 @@ async def remove_snapshot(request):
try:
target = request.rel_url.query["target"]
# Prevent path traversal attacks
path = manager_security.get_safe_file_path(target, context.manager_snapshot_path)
if path is None:
logging.warning(f"[Security] Invalid snapshot target rejected: {target}")
return web.Response(text="Invalid target", status=400)
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
os.remove(path)
@ -1187,12 +1177,7 @@ async def restore_snapshot(request):
try:
target = request.rel_url.query["target"]
# Prevent path traversal attacks
path = manager_security.get_safe_file_path(target, context.manager_snapshot_path)
if path is None:
logging.warning(f"[Security] Invalid snapshot target rejected: {target}")
return web.Response(text="Invalid target", status=400)
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
if not os.path.exists(context.manager_startup_script_path):
os.makedirs(context.manager_startup_script_path)

View File

@ -340,13 +340,10 @@ try:
pass
with std_log_lock:
try:
if self.is_stdout:
original_stdout.flush()
else:
original_stderr.flush()
except (OSError, ValueError):
pass
if self.is_stdout:
original_stdout.flush()
else:
original_stderr.flush()
def close(self):
self.flush()

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "comfyui-manager"
license = { text = "GPL-3.0-only" }
version = "4.0.5"
version = "4.0.3b7"
requires-python = ">= 3.9"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
readme = "README.md"