From aaed1dc3d568f012a98c9054acc4450e57e4456c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" <128333288+ltdrdata@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:42:12 +0900 Subject: [PATCH] feat(security): Support System User Protection API with security migration (V3.38) (#2338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate Manager data path: default/ComfyUI-Manager → __manager - Force security_level=strong on outdated ComfyUI (block installations) - Auto-migrate config.ini only; backup legacy files for manual verification - Raise weak/normal- to normal during migration - Add /manager/startup_alerts API for UI warnings - Differentiate 403 responses: comfyui_outdated vs security_level - Block startup scripts execution on old ComfyUI Requires ComfyUI v0.3.76+ for full functionality. Backward compatible with older versions (uses legacy path). --- README.md | 32 +- docs/en/v3.38-userdata-security-migration.md | 230 ++++++++++++ glob/manager_core.py | 18 +- glob/manager_migration.py | 356 +++++++++++++++++++ glob/manager_server.py | 54 ++- js/cm-api.js | 6 +- js/comfyui-manager.js | 40 ++- js/common.js | 40 ++- js/custom-nodes-manager.js | 13 +- js/model-manager.js | 17 +- js/snapshot.js | 10 +- prestartup_script.py | 19 +- pyproject.toml | 2 +- 13 files changed, 778 insertions(+), 59 deletions(-) create mode 100644 docs/en/v3.38-userdata-security-migration.md create mode 100644 glob/manager_migration.py diff --git a/README.md b/README.md index 65e986f2..ea4919a7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/refs/heads/Main/ComfyUI-Manager/images/dialog.jpg) ## NOTICE +* V3.38: **Security patch** - Manager data migrated to protected path. See [Migration Guide](docs/en/v3.38-userdata-security-migration.md). * V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`. * V3.10: `double-click feature` is removed * This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper @@ -140,20 +141,27 @@ This repository provides Colab notebooks that allow you to install and use Comfy ## Paths -In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generated files are located under `/default/ComfyUI-Manager/`. +Starting from V3.38, Manager uses a protected system path for enhanced security. -* - * If executed without any options, the path defaults to ComfyUI/user. - * It can be set using --user-directory . +* + * If executed without any options, the path defaults to ComfyUI/user. + * It can be set using --user-directory . -* Basic config files: `/default/ComfyUI-Manager/config.ini` -* Configurable channel lists: `/default/ComfyUI-Manager/channels.ini` -* Configurable pip overrides: `/default/ComfyUI-Manager/pip_overrides.json` -* Configurable pip blacklist: `/default/ComfyUI-Manager/pip_blacklist.list` -* Configurable pip auto fix: `/default/ComfyUI-Manager/pip_auto_fix.list` -* Saved snapshot files: `/default/ComfyUI-Manager/snapshots` -* Startup script files: `/default/ComfyUI-Manager/startup-scripts` -* Component files: `/default/ComfyUI-Manager/components` +| ComfyUI Version | Manager Path | +|-----------------|--------------| +| v0.3.76+ (with System User API) | `/__manager/` | +| Older versions | `/default/ComfyUI-Manager/` | + +* Basic config files: `config.ini` +* Configurable channel lists: `channels.list` +* Configurable pip overrides: `pip_overrides.json` +* Configurable pip blacklist: `pip_blacklist.list` +* Configurable pip auto fix: `pip_auto_fix.list` +* Saved snapshot files: `snapshots/` +* Startup script files: `startup-scripts/` +* Component files: `components/` + +> **Note**: See [Migration Guide](docs/en/v3.38-userdata-security-migration.md) for upgrade details. ## `extra_model_paths.yaml` Configuration diff --git a/docs/en/v3.38-userdata-security-migration.md b/docs/en/v3.38-userdata-security-migration.md new file mode 100644 index 00000000..324321d7 --- /dev/null +++ b/docs/en/v3.38-userdata-security-migration.md @@ -0,0 +1,230 @@ +# ComfyUI-Manager V3.38: Userdata Security Migration Guide + +## Introduction + +ComfyUI-Manager V3.38 introduces a **security patch** that migrates Manager's configuration and data to a protected system path. This change leverages ComfyUI's new System User Protection API (PR #10966) to provide enhanced security isolation. + +This guide explains what happens during the migration and how to handle various situations. + +--- + +## What Changed + +### Finding Your Paths + +When ComfyUI starts, it displays the full paths in the terminal: + +``` +** User directory: /path/to/ComfyUI/user +** ComfyUI-Manager config path: /path/to/ComfyUI/user/__manager/config.ini +``` + +Look for these lines in your startup log to find the exact location on your system. In this guide, paths are shown relative to the `user` directory. + +### Path Migration + +| Data | Legacy Path | New Path | +|------|-------------|----------| +| Configuration | `user/default/ComfyUI-Manager/` | `user/__manager/` | +| Snapshots | `user/default/ComfyUI-Manager/snapshots/` | `user/__manager/snapshots/` | + +### Why This Change + +In older ComfyUI versions, the `default/` directory was **unprotected** and accessible via web APIs. If you ran ComfyUI with `--listen 0.0.0.0` or similar options to allow external connections, this data **may have been tampered with** by malicious actors. + +**Note:** If you only used ComfyUI locally (without `--listen` or with `--listen 127.0.0.1`), your data was not exposed to this vulnerability. + +The new `__manager` path uses ComfyUI's protected system directory, which: +- **Cannot be accessed** from outside (protected by ComfyUI) +- Isolates system settings from user data +- Enables stricter security for remote access + +**This is why only `config.ini` is automatically migrated** - other files (snapshots) may have been compromised and should be manually verified before copying. + +--- + +## Automatic Migration + +When you start ComfyUI with the new System User Protection API, Manager automatically handles the migration: + +### Step 1: Configuration Migration + +Only `config.ini` is migrated automatically. + +**Important**: Snapshots are **NOT** automatically migrated. You must copy them manually if needed. + +### Step 2: Security Level Check + +During migration, if your security level is below `normal` (i.e., `weak` or `normal-`), it will be automatically raised to `normal`. This is a safety measure because the security level setting itself may have been tampered with in the old version. + +``` +====================================================================== +[ComfyUI-Manager] WARNING: Security level adjusted + - Previous: 'weak' → New: 'normal' + - Raised to prevent unauthorized remote access. +====================================================================== +``` + +If you need a lower security level, you can manually edit the config after migration. + +### Step 3: Legacy Backup + +Your entire legacy directory is moved to a backup location: +``` +user/__manager/.legacy-manager-backup/ +``` + +This backup is preserved until you manually delete it. + +--- + +## Persistent Backup Notification + +As long as the backup exists, Manager will remind you on **every startup**: + +``` +---------------------------------------------------------------------- +[ComfyUI-Manager] NOTICE: Legacy backup exists + - Your old Manager data was backed up to: + /path/to/ComfyUI/user/__manager/.legacy-manager-backup + - Please verify and remove it when no longer needed. +---------------------------------------------------------------------- +``` + +**To stop this notification**: Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it. + +--- + +## Recovering Old Data + +### Snapshots + +If you need your old snapshots, copy the contents of `.legacy-manager-backup/snapshots/` to `user/__manager/snapshots/`. + +--- + +## Outdated ComfyUI Warning + +If you're running an older version of ComfyUI without the System User Protection API, Manager will: + +1. **Force security level to `strong`** - All installations are blocked +2. **Display warning message**: + +``` +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +[ComfyUI-Manager] ERROR: ComfyUI version is outdated! + - Most operations are blocked for security. + - ComfyUI update is still allowed. + - Please update ComfyUI to use Manager normally. +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +``` + +**Solution**: Update ComfyUI to v0.3.76 or later. + +--- + +## Security Levels + +| Level | What's Allowed | +|-------|----------------| +| `strong` | ComfyUI update only. All other installations blocked. | +| `normal` | Install/update/remove registered custom nodes and models. | +| `normal-` | Above + Install via Git URL or pip (localhost only). | +| `weak` | All operations allowed, including from remote connections. | + +**Notes:** +- `strong` is forced on outdated ComfyUI versions. +- `normal` is the default and recommended for most users. +- `normal-` is for developers who need to install unregistered nodes locally. +- `weak` should only be used in isolated development environments. + +### Changing Security Level + +Edit `user/__manager/config.ini`: +```ini +[default] +security_level = normal +``` + +--- + +## Error Messages + +### "comfyui_outdated" (HTTP 403) + +This error appears when: +- Your ComfyUI doesn't have the System User Protection API +- All installations are blocked until you update ComfyUI + +**Solution**: Update ComfyUI to the latest version. + +### "security_level" (HTTP 403) + +This error appears when: +- Your security level blocks the requested operation +- For example, `strong` level blocks all installations + +**Solution**: Lower your security level in config.ini if appropriate for your use case. + +--- + +## Security Warning: Suspicious Path + +If you see this error on an **older** ComfyUI: + +``` +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +[ComfyUI-Manager] ERROR: Suspicious path detected! + - '__manager' exists with low security level: 'weak' + - Please verify manually: + /path/to/ComfyUI/user/__manager/config.ini +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +``` + +On older ComfyUI versions, the `__manager` directory is not normally created. If this directory exists, it may have been created externally. For safety, manually verify the contents of this directory before updating ComfyUI. + +--- + +## Troubleshooting + +### All my installations are blocked + +**Check 1**: Is your ComfyUI updated? +- Old ComfyUI forces `security_level = strong` +- Update ComfyUI to resolve + +**Check 2**: What's your security level? +- Check `user/__manager/config.ini` +- `security_level = strong` blocks all installations + +### My snapshots are missing + +Snapshots are not automatically migrated. You need to manually copy the `snapshots` folder from inside `.legacy-manager-backup` to the `user/__manager/` directory. + +### I keep seeing the backup notification + +Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it. + +### Snapshot restore is blocked + +On old ComfyUI (without System User API), snapshot restore is blocked because security is forced to `strong`. Update ComfyUI to enable snapshot restore. + +--- + +## File Structure Reference + +``` +user/ +└── __manager/ + ├── config.ini # Manager configuration + ├── channels.list # Custom node channels + ├── snapshots/ # Environment snapshots + └── .legacy-manager-backup/ # Backup of old Manager data (temporary) +``` + +--- + +## Requirements + +- **ComfyUI**: v0.3.76 or later (with System User Protection API) +- **ComfyUI-Manager**: V3.38 or later diff --git a/glob/manager_core.py b/glob/manager_core.py index 67570786..460631a4 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -40,10 +40,11 @@ import cnr_utils import manager_util import git_utils import manager_downloader +import manager_migration from node_package import InstalledNodePackage -version_code = [3, 37, 2] +version_code = [3, 38] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') @@ -214,9 +215,10 @@ def update_user_directory(user_dir): global manager_pip_blacklist_path global manager_components_path - manager_files_path = os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager')) + manager_files_path = manager_migration.get_manager_path(user_dir) if not os.path.exists(manager_files_path): os.makedirs(manager_files_path) + manager_migration.run_migration_checks(user_dir, manager_files_path) manager_snapshot_path = os.path.join(manager_files_path, "snapshots") if not os.path.exists(manager_snapshot_path): @@ -1719,7 +1721,7 @@ def read_config(): manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False manager_util.bypass_ssl = get_bool('bypass_ssl', False) - return { + result = { 'http_channel_enabled': get_bool('http_channel_enabled', False), 'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(), 'git_exe': default_conf.get('git_exe', ''), @@ -1739,6 +1741,8 @@ def read_config(): 'security_level': default_conf.get('security_level', 'normal').lower(), 'db_mode': default_conf.get('db_mode', 'cache').lower(), } + manager_migration.force_security_level_if_needed(result) + return result except Exception: import importlib.util @@ -1746,7 +1750,7 @@ def read_config(): manager_util.use_uv = importlib.util.find_spec("uv") is not None and platform.system() != "Windows" manager_util.bypass_ssl = False - return { + result = { 'http_channel_enabled': False, 'preview_method': manager_funcs.get_current_preview_method(), 'git_exe': '', @@ -1766,6 +1770,8 @@ def read_config(): 'security_level': 'normal', # strong | normal | normal- | weak 'db_mode': 'cache', # local | cache | remote } + manager_migration.force_security_level_if_needed(result) + return result def get_config(): @@ -3360,8 +3366,8 @@ def get_comfyui_versions(repo=None): repo = git.Repo(comfy_path) try: - remote = get_remote_name(repo) - repo.remotes[remote].fetch() + remote = get_remote_name(repo) + repo.remotes[remote].fetch() except: logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI") diff --git a/glob/manager_migration.py b/glob/manager_migration.py new file mode 100644 index 00000000..d3dbd656 --- /dev/null +++ b/glob/manager_migration.py @@ -0,0 +1,356 @@ +""" +ComfyUI-Manager migration module. +Handles migration from legacy paths to new __manager path structure. +""" + +import os +import sys +import subprocess +import configparser + +# Startup notices for notice board +startup_notices = [] # List of (message, level) tuples + + +def add_startup_notice(message, level='warning'): + """Add a notice to be displayed on Manager notice board. + + Args: + message: HTML-formatted message string + level: 'warning', 'error', 'info' + """ + global startup_notices + startup_notices.append((message, level)) + + +# Cache for API check (computed once per session) +_cached_has_system_user_api = None + + +def has_system_user_api(): + """Check if ComfyUI has the System User Protection API (PR #10966). + + Result is cached for performance. + """ + global _cached_has_system_user_api + if _cached_has_system_user_api is None: + try: + import folder_paths + _cached_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory') + except Exception: + _cached_has_system_user_api = False + return _cached_has_system_user_api + + +def get_manager_path(user_dir): + """Get the appropriate manager files path based on ComfyUI version. + + Returns: + str: manager_files_path + """ + if has_system_user_api(): + return os.path.abspath(os.path.join(user_dir, '__manager')) + else: + return os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager')) + + +def run_migration_checks(user_dir, manager_files_path): + """Run all migration and security checks. + + Call this after get_manager_path() to handle: + - Legacy config migration (new ComfyUI) + - Legacy backup notification (every startup) + - Suspicious directory detection (old ComfyUI) + - Outdated ComfyUI warning (old ComfyUI) + """ + if has_system_user_api(): + migrated = migrate_legacy_config(user_dir, manager_files_path) + # Only check for legacy backup if migration didn't just happen + # (migration already shows backup location in its message) + if not migrated: + check_legacy_backup(manager_files_path) + else: + check_suspicious_manager(user_dir) + warn_outdated_comfyui() + + +def check_legacy_backup(manager_files_path): + """Check for legacy backup and notify user to verify and remove it. + + This runs on every startup to remind users about pending legacy backup. + """ + backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup') + if not os.path.exists(backup_dir): + return + + # Terminal output + print("\n" + "-"*70) + print("[ComfyUI-Manager] NOTICE: Legacy backup exists") + print(" - Your old Manager data was backed up to:") + print(f" {backup_dir}") + print(" - Please verify and remove it when no longer needed.") + print("-"*70 + "\n") + + # Notice board output + add_startup_notice( + "Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed.", + level='info' + ) + + +def check_suspicious_manager(user_dir): + """Check for suspicious __manager directory on old ComfyUI. + + On old ComfyUI without System User API, if __manager exists with low security, + warn the user to verify manually. + + Returns: + bool: True if suspicious setup detected + """ + if has_system_user_api(): + return False # Not suspicious on new ComfyUI + + suspicious_path = os.path.abspath(os.path.join(user_dir, '__manager')) + if not os.path.exists(suspicious_path): + return False + + config_path = os.path.join(suspicious_path, 'config.ini') + if not os.path.exists(config_path): + return False + + config = configparser.ConfigParser() + config.read(config_path) + sec_level = config.get('default', 'security_level', fallback='normal').lower() + + if sec_level in ['weak', 'normal-']: + # Terminal output + print("\n" + "!"*70) + print("[ComfyUI-Manager] ERROR: Suspicious path detected!") + print(f" - '__manager' exists with low security level: '{sec_level}'") + print(" - Please verify manually:") + print(f" {config_path}") + print("!"*70 + "\n") + + # Notice board output + add_startup_notice( + "[Security Alert] Suspicious path detected. See terminal log for details.", + level='error' + ) + return True + + return False + + +def warn_outdated_comfyui(): + """Warn user about outdated ComfyUI without System User API.""" + if has_system_user_api(): + return + + # Terminal output + print("\n" + "!"*70) + print("[ComfyUI-Manager] ERROR: ComfyUI version is outdated!") + print(" - Most operations are blocked for security.") + print(" - ComfyUI update is still allowed.") + print(" - Please update ComfyUI to use Manager normally.") + print("!"*70 + "\n") + + # Notice board output + add_startup_notice( + "[Security Alert] ComfyUI outdated. Installations blocked (update allowed).
" + "Update ComfyUI for normal operation.", + level='error' + ) + + +def migrate_legacy_config(user_dir, manager_files_path): + """Migrate ONLY config.ini to new __manager path if needed. + + IMPORTANT: Only config.ini is migrated. Other files (snapshots, cache, etc.) + are NOT migrated - users must recreate them. + + Scenarios: + 1. Legacy exists, New doesn't exist → Migrate config.ini + 2. Legacy exists, New exists → First update after upgrade + - Run ComfyUI dependency installation + - Rename legacy to .backup + 3. Legacy doesn't exist → No migration needed + + Returns: + bool: True if migration was performed + """ + if not has_system_user_api(): + return False + + legacy_dir = os.path.join(user_dir, 'default', 'ComfyUI-Manager') + legacy_config = os.path.join(legacy_dir, 'config.ini') + new_config = os.path.join(manager_files_path, 'config.ini') + + if not os.path.exists(legacy_dir): + return False # No legacy directory, nothing to migrate + + # IMPORTANT: Check for config.ini existence, not just directory + # (because makedirs() creates __manager before this function is called) + + # Case: Both configs exist (first update after ComfyUI upgrade) + # This means user ran new ComfyUI at least once, creating __manager/config.ini + if os.path.exists(legacy_config) and os.path.exists(new_config): + _handle_first_update_migration(user_dir, legacy_dir, manager_files_path) + return True + + # Case: Legacy config exists but new config doesn't (normal migration) + # This is the first run after ComfyUI upgrade + if os.path.exists(legacy_config) and not os.path.exists(new_config): + pass # Continue with normal migration below + else: + return False + + # Terminal output + print("\n" + "-"*70) + print("[ComfyUI-Manager] NOTICE: Legacy config.ini detected") + print(f" - Old: {legacy_config}") + print(f" - New: {new_config}") + print(" - Migrating config.ini only (other files are NOT migrated).") + print(" - Security level below 'normal' will be raised.") + print("-"*70 + "\n") + + _migrate_config_with_security_check(legacy_config, new_config) + + # Move legacy directory to backup + _move_legacy_to_backup(legacy_dir, manager_files_path) + + return True + + +def _handle_first_update_migration(user_dir, legacy_dir, manager_files_path): + """Handle first ComfyUI update when both legacy and new directories exist. + + This scenario happens when: + - User was on old ComfyUI (using default/ComfyUI-Manager) + - ComfyUI was updated (now has System User API) + - Manager already created __manager on first new run + - But legacy directory still exists + + Actions: + 1. Run ComfyUI dependency installation + 2. Move legacy to __manager/.legacy-manager-backup + """ + # Terminal output + print("\n" + "-"*70) + print("[ComfyUI-Manager] NOTICE: First update after ComfyUI upgrade detected") + print(" - Both legacy and new directories exist.") + print(" - Running ComfyUI dependency installation...") + print("-"*70 + "\n") + + # Run ComfyUI dependency installation + # Path: glob/manager_migration.py → glob → comfyui-manager → custom_nodes → ComfyUI + try: + comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + requirements_path = os.path.join(comfyui_path, 'requirements.txt') + if os.path.exists(requirements_path): + subprocess.run([sys.executable, '-m', 'pip', 'install', '-r', requirements_path], + capture_output=True, check=False) + print("[ComfyUI-Manager] ComfyUI dependencies installation completed.") + except Exception as e: + print(f"[ComfyUI-Manager] WARNING: Failed to install ComfyUI dependencies: {e}") + + # Move legacy to backup inside __manager + _move_legacy_to_backup(legacy_dir, manager_files_path) + + +def _move_legacy_to_backup(legacy_dir, manager_files_path): + """Move legacy directory to backup inside __manager. + + Returns: + str: Path to backup directory if successful, None if failed + """ + import shutil + + backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup') + + try: + if os.path.exists(backup_dir): + shutil.rmtree(backup_dir) # Remove old backup if exists + shutil.move(legacy_dir, backup_dir) + + # Terminal output (full paths shown here only) + print("\n" + "-"*70) + print("[ComfyUI-Manager] NOTICE: Legacy settings migrated") + print(f" - Old location: {legacy_dir}") + print(f" - Backed up to: {backup_dir}") + print(" - Please verify and remove the backup when no longer needed.") + print("-"*70 + "\n") + + # Notice board output (no full paths for security) + add_startup_notice( + "Legacy ComfyUI-Manager data migrated. See terminal for details.", + level='info' + ) + return backup_dir + except Exception as e: + print(f"[ComfyUI-Manager] WARNING: Failed to backup legacy directory: {e}") + add_startup_notice( + f"[MIGRATION] Failed to backup legacy directory: {e}", + level='warning' + ) + return None + + +def _migrate_config_with_security_check(legacy_path, new_path): + """Migrate legacy config, raising security level only if below default.""" + config = configparser.ConfigParser() + try: + config.read(legacy_path) + except Exception as e: + print(f"[ComfyUI-Manager] WARNING: Failed to parse config.ini: {e}") + print(" - Creating fresh config with default settings.") + add_startup_notice( + "[MIGRATION] Failed to parse legacy config. Using defaults.", + level='warning' + ) + return # Skip migration, let Manager create fresh config + + # Security level hierarchy: strong > normal > normal- > weak + # Default is 'normal', only raise if below default + if 'default' in config: + current_level = config['default'].get('security_level', 'normal').lower() + below_default_levels = ['weak', 'normal-'] + + if current_level in below_default_levels: + config['default']['security_level'] = 'normal' + + # Terminal output + print("\n" + "="*70) + print("[ComfyUI-Manager] WARNING: Security level adjusted") + print(f" - Previous: '{current_level}' → New: 'normal'") + print(" - Raised to prevent unauthorized remote access.") + print("="*70 + "\n") + + # Notice board output + add_startup_notice( + f"[MIGRATION] Security level raised: '{current_level}' → 'normal'.
" + "To prevent unauthorized remote access.", + level='warning' + ) + else: + print(f" - Security level: '{current_level}' (no change needed)") + + # Ensure directory exists + os.makedirs(os.path.dirname(new_path), exist_ok=True) + + with open(new_path, 'w') as f: + config.write(f) + + +def force_security_level_if_needed(config_dict): + """Force security level to 'strong' if on old ComfyUI. + + Args: + config_dict: Configuration dictionary to modify in-place + + Returns: + bool: True if security level was forced + """ + if not has_system_user_api(): + config_dict['security_level'] = 'strong' + return True + return False diff --git a/glob/manager_server.py b/glob/manager_server.py index cb3bcd92..de2f1a89 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -22,6 +22,7 @@ import asyncio import queue import manager_downloader +import manager_migration logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})") @@ -276,6 +277,13 @@ import zipfile import urllib.request +def security_403_response(): + """Return appropriate 403 response based on ComfyUI version.""" + if not manager_migration.has_system_user_api(): + return web.json_response({"error": "comfyui_outdated"}, status=403) + return web.json_response({"error": "security_level"}, status=403) + + def get_model_dir(data, show_log=False): if 'download_model_base' in folder_paths.folder_names_and_paths: models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0] @@ -732,7 +740,7 @@ async def fetch_updates(request): async def update_all(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return security_403_response() with task_worker_lock: is_processing = task_worker_thread is not None and task_worker_thread.is_alive() @@ -965,7 +973,7 @@ async def get_snapshot_list(request): async def remove_snapshot(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return security_403_response() try: target = request.rel_url.query["target"] @@ -983,7 +991,7 @@ async def remove_snapshot(request): async def restore_snapshot(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return security_403_response() try: target = request.rel_url.query["target"] @@ -1302,7 +1310,7 @@ async def fix_custom_node(request): async def install_custom_node_git_url(request): if not is_allowed_security_level('high'): logging.error(SECURITY_MESSAGE_NORMAL_MINUS) - return web.Response(status=403) + return security_403_response() url = await request.text() res = await core.gitclone_install(url) @@ -1322,7 +1330,7 @@ async def install_custom_node_git_url(request): async def install_custom_node_pip(request): if not is_allowed_security_level('high'): logging.error(SECURITY_MESSAGE_NORMAL_MINUS) - return web.Response(status=403) + return security_403_response() packages = await request.text() core.pip_install(packages.split(' ')) @@ -1594,6 +1602,16 @@ async def get_notice(request): except: pass + # Prepend startup notices from manager_migration + for message, level in reversed(manager_migration.startup_notices): + if level == 'error': + style = 'color:red; background-color:white; font-weight:bold' + elif level == 'warning': + style = 'color:orange; background-color:white; font-weight:bold' + else: + style = 'color:blue; background-color:white' + markdown_content = f'

{message}

' + markdown_content + return web.Response(text=markdown_content, status=200) else: return web.Response(text="Unable to retrieve Notice", status=200) @@ -1601,11 +1619,35 @@ async def get_notice(request): return web.Response(text="Unable to retrieve Notice", status=200) +@routes.get("/manager/startup_alerts") +async def get_startup_alerts(request): + """Return startup alerts for customAlert display on page load. + + Returns JSON array of alerts that should be shown to user immediately. + All startup notices (error, warning, info) are returned. + """ + alerts = [] + + # Return all startup notices for alert display + for message, level in manager_migration.startup_notices: + # Convert HTML BR to newlines for customAlert + text = message.replace('
', '\n').replace('
', '\n') + # Add [ComfyUI-Manager] prefix for customAlert (notice board shows in Manager UI anyway) + text = text.replace('[Security Alert]', '[ComfyUI-Manager] Security Alert:') + text = text.replace('[MIGRATION]', '[ComfyUI-Manager] Migration:') + alerts.append({ + 'message': text, + 'level': level + }) + + return web.json_response(alerts) + + @routes.get("/manager/reboot") def restart(self): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return security_403_response() try: sys.stdout.close_log() diff --git a/js/cm-api.js b/js/cm-api.js index dabc6f1d..c4d6da03 100644 --- a/js/cm-api.js +++ b/js/cm-api.js @@ -1,6 +1,6 @@ import { api } from "../../scripts/api.js"; import { app } from "../../scripts/app.js"; -import { sleep, customConfirm, customAlert } from "./common.js"; +import { sleep, customConfirm, customAlert, handle403Response, show_message } from "./common.js"; async function tryInstallCustomNode(event) { let msg = '-= [ComfyUI Manager] extension installation request =-\n\n'; @@ -42,7 +42,7 @@ async function tryInstallCustomNode(event) { }); if(response.status == 403) { - show_message('This action is not allowed with this security level configuration.'); + await handle403Response(response); return false; } else if(response.status == 400) { @@ -54,7 +54,7 @@ async function tryInstallCustomNode(event) { let response = await api.fetchApi("/manager/reboot"); if(response.status == 403) { - show_message('This action is not allowed with this security level configuration.'); + await handle403Response(response); return false; } diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 6fc504b1..e65a2c65 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -14,7 +14,7 @@ import { OpenArtShareDialog } from "./comfyui-share-openart.js"; import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message, customAlert, customPrompt, - infoToast, showTerminal, setNeedRestart + infoToast, showTerminal, setNeedRestart, handle403Response } from "./common.js"; import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js"; import { CustomNodesManager } from "./custom-nodes-manager.js"; @@ -753,9 +753,9 @@ async function onQueueStatus(event) { const rebootButton = document.getElementById('cm-reboot-button5'); rebootButton?.addEventListener("click", - function() { - if(rebootAPI()) { - manager_dialog.close(); + async function() { + if(await rebootAPI()) { + manager_instance.close(); } }); } @@ -780,8 +780,13 @@ async function updateAll(update_comfyui) { const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`); - if (response.status == 401) { + if (response.status == 403) { + await handle403Response(response); + reset_action_buttons(); + } + else if (response.status == 401) { customAlert('Another task is already in progress. Please stop the ongoing task first.'); + reset_action_buttons(); } else if(response.status == 200) { is_updating = true; @@ -1453,6 +1458,31 @@ app.registerExtension({ load_components(); + // Fetch and show startup alerts (critical errors like outdated ComfyUI) + // Poll until extensionManager.toast is ready (set in Vue onMounted) + const showStartupAlerts = async () => { + let toastWaitCount = 0; + const waitForToast = () => { + if (window['app']?.extensionManager?.toast) { + fetch('/manager/startup_alerts') + .then(response => response.ok ? response.json() : []) + .then(alerts => { + for (const alert of alerts) { + customAlert(alert.message); + } + }) + .catch(e => console.warn('[ComfyUI-Manager] Failed to fetch startup alerts:', e)); + } else if (toastWaitCount < 300) { // Max 30 seconds (300 * 100ms) + toastWaitCount++; + setTimeout(waitForToast, 100); + } else { + console.warn('[ComfyUI-Manager] Timeout waiting for toast. Startup alerts skipped.'); + } + }; + waitForToast(); + }; + showStartupAlerts(); + const menu = document.querySelector(".comfy-menu"); const separator = document.createElement("hr"); diff --git a/js/common.js b/js/common.js index 71cf58ea..b8193055 100644 --- a/js/common.js +++ b/js/common.js @@ -100,6 +100,19 @@ export function show_message(msg) { app.ui.dialog.element.style.zIndex = 1100; } +export async function handle403Response(res, defaultMessage) { + try { + const data = await res.json(); + if(data.error === 'comfyui_outdated') { + show_message('ComfyUI version is outdated.
Please update ComfyUI to use Manager normally.'); + } else { + show_message(defaultMessage || 'This action is not allowed with this security level configuration.'); + } + } catch { + show_message(defaultMessage || 'This action is not allowed with this security level configuration.'); + } +} + export async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -163,20 +176,23 @@ export async function customPrompt(title, message) { } -export function rebootAPI() { +export async function rebootAPI() { if ('electronAPI' in window) { window.electronAPI.restartApp(); return true; } - customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => { - if (isConfirmed) { - try { - api.fetchApi("/manager/reboot"); + const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?"); + if (isConfirmed) { + try { + const response = await api.fetchApi("/manager/reboot"); + if (response.status == 403) { + await handle403Response(response); + return false; } - catch(exception) {} } - }); + catch(exception) {} + } return false; } @@ -216,7 +232,7 @@ export async function install_pip(packages) { }); if(res.status == 403) { - show_message('This action is not allowed with this security level configuration.'); + await handle403Response(res); return; } @@ -251,7 +267,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.'); + await handle403Response(res); return; } @@ -262,9 +278,9 @@ export async function install_via_git_url(url, manager_dialog) { const self = this; rebootButton.addEventListener("click", - function() { - if(rebootAPI()) { - manager_dialog.close(); + async function() { + if(await rebootAPI()) { + manager_instance.close(); } }); } diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index a5683a2d..a4ca3653 100644 --- a/js/custom-nodes-manager.js +++ b/js/custom-nodes-manager.js @@ -7,7 +7,7 @@ import { fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML, infoToast, showTerminal, setNeedRestart, storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss, - showPopover, hidePopover + showPopover, hidePopover, handle403Response } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html @@ -1528,7 +1528,16 @@ export class CustomNodesManager { errorMsg = `'${item.title}': `; if(res.status == 403) { - errorMsg += `This action is not allowed with this security level configuration.\n`; + try { + const data = await res.json(); + if(data.error === 'comfyui_outdated') { + errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`; + } else { + errorMsg += `This action is not allowed with this security level configuration.\n`; + } + } catch { + errorMsg += `This action is not allowed with this security level configuration.\n`; + } } else if(res.status == 404) { errorMsg += `With the current security level configuration, only custom nodes from the "default channel" can be installed.\n`; } else { diff --git a/js/model-manager.js b/js/model-manager.js index 7811ab65..076dbb78 100644 --- a/js/model-manager.js +++ b/js/model-manager.js @@ -1,9 +1,9 @@ import { app } from "../../scripts/app.js"; import { $el } from "../../scripts/ui.js"; -import { - manager_instance, rebootAPI, +import { + manager_instance, rebootAPI, fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal, - storeColumnWidth, restoreColumnWidth, loadCss + storeColumnWidth, restoreColumnWidth, loadCss, handle403Response } from "./common.js"; import { api } from "../../scripts/api.js"; @@ -477,7 +477,16 @@ export class ModelManager { errorMsg = `'${item.name}': `; if(res.status == 403) { - errorMsg += `This action is not allowed with this security level configuration.\n`; + try { + const data = await res.json(); + if(data.error === 'comfyui_outdated') { + errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`; + } else { + errorMsg += `This action is not allowed with this security level configuration.\n`; + } + } catch { + errorMsg += `This action is not allowed with this security level configuration.\n`; + } } else { errorMsg += await res.text() + '\n'; } diff --git a/js/snapshot.js b/js/snapshot.js index 520ca615..5e2900fa 100644 --- a/js/snapshot.js +++ b/js/snapshot.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { manager_instance, rebootAPI, show_message } from "./common.js"; +import { manager_instance, rebootAPI, show_message, handle403Response } from "./common.js"; async function restore_snapshot(target) { @@ -10,7 +10,7 @@ async function restore_snapshot(target) { const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" }); if(response.status == 403) { - show_message('This action is not allowed with this security level configuration.'); + await handle403Response(response); return false; } @@ -38,7 +38,7 @@ async function remove_snapshot(target) { const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" }); if(response.status == 403) { - show_message('This action is not allowed with this security level configuration.'); + await handle403Response(response); return false; } @@ -145,8 +145,8 @@ export class SnapshotManager extends ComfyDialog { if(btn_id) { const rebootButton = document.getElementById(btn_id); const self = this; - rebootButton.onclick = function() { - if(rebootAPI()) { + rebootButton.onclick = async function() { + if(await rebootAPI()) { self.close(); self.manager_dialog.close(); } diff --git a/prestartup_script.py b/prestartup_script.py index f3e52ea4..272cdd6a 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -85,7 +85,15 @@ cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extensi comfyui_manager_path = os.path.abspath(os.path.dirname(__file__)) custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0] -manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager')) + +# Check for System User API availability (PR #10966) +_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory') + +if _has_system_user_api: + manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), '__manager')) +else: + manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager')) + manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json") manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list") restore_snapshot_path = os.path.join(manager_files_path, "startup-scripts", "restore-snapshot.json") @@ -516,7 +524,8 @@ check_bypass_ssl() # Perform install processed_install = set() -script_list_path = os.path.join(folder_paths.user_directory, "default", "ComfyUI-Manager", "startup-scripts", "install-scripts.txt") +# Use manager_files_path for consistency (fixes path inconsistency bug) +script_list_path = os.path.join(manager_files_path, "startup-scripts", "install-scripts.txt") pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path) @@ -793,7 +802,11 @@ def execute_startup_script(): # Check if script_list_path exists -if os.path.exists(script_list_path): +# Block startup-scripts on old ComfyUI (security measure) +if not _has_system_user_api: + if os.path.exists(script_list_path): + print("[ComfyUI-Manager] Startup scripts blocked on old ComfyUI version.") +elif os.path.exists(script_list_path): execute_startup_script() diff --git a/pyproject.toml b/pyproject.toml index 6c18645b..5ebbb627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-manager" description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." -version = "3.37.2" +version = "3.38" license = { file = "LICENSE.txt" } dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]