CVE-2025-67303: Verified Repro With Script Download
CVE-2025-67303: ComfyUI-Manager: Configuration File Exposure via Web-Accessible Path
CVE-2025-67303 is verified against ComfyUI-Manager · pip. Affected versions: < 3.38. Fixed in 3.38. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00052.
What Is CVE-2025-67303?
CVE-2025-67303 (GHSA-2hc9-cc65-xwj8) is a high-severity configuration-exposure vulnerability in ComfyUI-Manager (before 3.38), where the extension's configuration and other critical data are stored in a location served by the ComfyUI web interface. Pruva reproduced it (reproduction REPRO-2026-00052).
CVE-2025-67303 Severity & CVSS Score
CVE-2025-67303 is rated high severity, with a CVSS base score of 7.5 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected ComfyUI-Manager Versions
ComfyUI-Manager · pip versions < 3.38 are affected.
How to Reproduce CVE-2025-67303
pruva-verify REPRO-2026-00052 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00052/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2025-67303
Reproduced by Pruva's autonomous agents — 57 tool calls over 12 min. Full root-cause analysis and the complete transcript are below.
How the agent worked
Root Cause and Exploit Chain for CVE-2025-67303
Prior to 3.38, ComfyUI-Manager stored its configuration/critical data in a location served by the ComfyUI web interface, enabling remote unauthenticated access and potential overwrite/manipulation via HTTP.
CVE-2025-67303 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
python3 - << 'PY'
import sys, json, os, subprocess, textwrap
print('Searching web...')
PYSearching web...
curl -sSL https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/docs/en/v3.38-userdata-security-migration.md | sed -n '1,200p'{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":138,"exit_code":0,"stderr":"","stdout":"# ComfyUI-Manager V3.38: Userdata Security Migration Guide\n\n## Introduction\n\nComfyUI-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.\n\nThis guide explains what happens during the migration and how to handle various situations.\n\n---\n\n## What Changed\n\n### Finding Your Paths\n\nWhen ComfyUI starts, it displays the full paths in the terminal:\n\n```\n** User directory: /path/to/ComfyUI/user\n** ComfyUI-Manager config path: /path/to/ComfyUI/user/__manager/config.ini\n```\n\nLook 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.\n\n### Path Migration\n\n| Data | Legacy Path | New Path |\n|------|-------------|----------|\n| Configuration | `user/default/ComfyUI-Manager/` | `user/__manager/` |\n| Snapshots | `user/default/ComfyUI-Manager/snapshots/` | `user/__manager/snapshots/` |\n\n##… [truncated]grep -n "ComfyUI file API" -R /dev/null || truegit clone --depth 1 https://github.com/comfyanonymous/ComfyUI.git comfyuirepoCloning into 'comfyuirepo'...
git clone --depth 1 https://github.com/Comfy-Org/ComfyUI-Manager.git managerrepoCloning into 'managerrepo'...
git -C managerrepo log -1 --pretty=oneline8d67702ab0fe90d5a1fe8d18f14edfc522f97bbb update DB
git -C managerrepo tag --sort=version:refname | tail -n 10grep -RIn "upload" comfyuirepo | headcomfyuirepo/comfy/cli_args.py:43:parser.add_argument("--max-upload-size", type=float, default=100, help="Set the maximum upload size in MB.")
comfyuirepo/comfy/comfy_types/node_typing.py:157: image_upload: NotRequired[bool]
comfyuirepo/comfy/comfy_types/node_typing.py:158: """Specifies whether the input should have an image upload button and image preview attached to it. Requires that the input's name is `image`."""
comfyuirepo/comfy/comfy_types/node_typing.py:160: """Specifies which folder to get preview images from if the input has the ``image_upload`` flag.
comfyuirepo/comfy/text_encoders/hydit_clip_tokenizer/vocab.txt:38803:uploaded
comfyuirepo/comfy/text_encoders/qwen25_tokenizer/merges.txt:19120:upload s
comfyuirepo/comfy/text_encoders/qwen25_tokenizer/merges.txt:21437:_ upload
comfyuirepo/comfy/text_encoders/qwen25_tokenizer/merges.txt:22599:Ġupload edpython3 - << 'PY'
import requests, sys
print('skip network within tool; using curl for docs')
PYskip network within tool; using curl for docs
grep -RIn "user" comfyuirepo | head -n 50{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":38,"exit_code":0,"stderr":"","stdout":"comfyuirepo/comfy/cli_args.py:45:parser.add_argument(\"--base-directory\", type=str, default=None, help=\"Set the ComfyUI base directory for models, custom_nodes, input, output, temp, and user directories.\")\ncomfyuirepo/comfy/cli_args.py:179:parser.add_argument(\"--multi-user\", action=\"store_true\", help=\"Enables per-user storage.\")\ncomfyuirepo/comfy/cli_args.py:219:parser.add_argument(\"--user-directory\", type=is_valid_directory, default=None, help=\"Set the ComfyUI user directory with an absolute path. Overrides --base-directory.\")\ncomfyuirepo/comfy/cli_args.py:231: os.path.join(os.path.dirname(__file__), \"..\", \"user\", \"comfyui.db\")\ncomfyuirepo/comfy/comfy_types/node_typing.py:236: \"\"\"Flags a node as experimental, informing users that it may change or not work as expected.\"\"\"\ncomfyuirepo/comfy/comfy_types/node_typing.py:238: \"\"\"Flags a node as deprecated, indicating to users that they should find alternatives to this node.\"\"\"\ncomfyuirepo/comfy/text_encoders/hunyuan_image.py:16: self.llama_template = \"<|im_start|>system… [truncated]grep -RIn "user/default" comfyuirepo | head -n 20comfyuirepo/folder_paths.py:194: '/path/to/user/default'
sed -n '1,260p' comfyuirepo/folder_paths.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":12,"exit_code":0,"stderr":"","stdout":"from __future__ import annotations\n\nimport os\nimport time\nimport mimetypes\nimport logging\nfrom typing import Literal, List\nfrom collections.abc import Collection\n\nfrom comfy.cli_args import args\n\nsupported_pt_extensions: set[str] = {'.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'}\n\nfolder_names_and_paths: dict[str, tuple[list[str], set[str]]] = {}\n\n# --base-directory - Resets all default paths configured in folder_paths with a new base path\nif args.base_directory:\n base_path = os.path.abspath(args.base_directory)\nelse:\n base_path = os.path.dirname(os.path.realpath(__file__))\n\nmodels_dir = os.path.join(base_path, \"models\")\nfolder_names_and_paths[\"checkpoints\"] = ([os.path.join(models_dir, \"checkpoints\")], supported_pt_extensions)\nfolder_names_and_paths[\"configs\"] = ([os.path.join(models_dir, \"configs\")], [\".yaml\"])\n\nfolder_names_and_paths[\"loras\"] = ([os.path.join(models_dir, \"loras\")], supported_pt_extensions)\nfolder_names_and_paths[\"vae\"] = ([os.path.join(models_dir, \"vae\")], supported_pt_exte… [truncated]grep -RIn "def .*upload" comfyuirepo | head -n 20comfyuirepo/server.py:373: def image_upload(post, image_save_function=None): comfyuirepo/server.py:422: async def upload_image(request): comfyuirepo/server.py:428: async def upload_mask(request): comfyuirepo/comfy_api_nodes/nodes_pixverse.py:33:async def upload_image_to_pixverse(cls: type[IO.ComfyNode], image: torch.Tensor): comfyuirepo/comfy_api_nodes/util/upload_helpers.py:44:async def upload_images_to_comfyapi( comfyuirepo/comfy_api_nodes/util/upload_helpers.py:77:async def upload_audio_to_comfyapi( comfyuirepo/comfy_api_nodes/util/upload_helpers.py:97:async def upload_video_to_comfyapi( comfyuirepo/comfy_api_nodes/util/upload_helpers.py:132:async def upload_file_to_comfyapi(
sed -n '360,520p' comfyuirepo/server.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":12,"exit_code":0,"stderr":"","stdout":" hasher = node_helpers.hasher()\n\n # function to compare hashes of two images to see if it already exists, fix to #3465\n if os.path.exists(filepath):\n a = hasher()\n b = hasher()\n with open(filepath, \"rb\") as f:\n a.update(f.read())\n b.update(image.file.read())\n image.file.seek(0)\n return a.hexdigest() == b.hexdigest()\n return False\n\n def image_upload(post, image_save_function=None):\n image = post.get(\"image\")\n overwrite = post.get(\"overwrite\")\n image_is_duplicate = False\n\n image_upload_type = post.get(\"type\")\n upload_dir, image_upload_type = get_dir_by_type(image_upload_type)\n\n if image and image.file:\n filename = image.filename\n if not filename:\n return web.Response(status=400)\n\n subfolder = post.get(\"subfolder\", \"\")\n full_… [truncated]grep -n "user/default" -R comfyuirepo | catcomfyuirepo/folder_paths.py:194: '/path/to/user/default'
grep -RIn "get_public_user_directory" comfyuirepo | cat{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":146,"exit_code":0,"stderr":"","stdout":"comfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:8:- Structural security: get_public_user_directory() provides automatic protection\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:336: - get_public_user_directory() automatically blocks System Users\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:340: def test_get_public_user_directory_blocks_system_user(self):\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:342: Any code using get_public_user_directory() is automatically protected.\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:346: assert folder_paths.get_public_user_directory(\"__system\") is None\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:347: assert folder_paths.get_public_user_directory(\"__cache\") is None\ncomfyuirepo/tests-unit/prompt_server_test/system_user_endpoint_test.py:348: assert folder_paths.get_public_user_directory(\"__anything\") is None\ncomfyuirepo/tests-unit/prompt_s… [truncated]grep -n "@routes.get\|@routes.post" -n comfyuirepo/server.py | sed -n '1,200p'245: @routes.get('/ws')
305: @routes.get("/")
313: @routes.get("/embeddings")
318: @routes.get("/models")
324: @routes.get("/models/{folder}")
332: @routes.get("/extensions")
421: @routes.post("/upload/image")
427: @routes.post("/upload/mask")grep -RIn "subfolder.*user" comfyuirepo/server.py || truegrep -RIn "ComfyUI-Manager" managerrepo | head -n 20{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":22,"exit_code":0,"stderr":"","stdout":"managerrepo/prestartup_script.py:36: logging.error(f\"[ComfyUI-Manager] fallback timestamp mode\\n datetime module is invalid: '{dt.__file__}'\")\nmanagerrepo/prestartup_script.py:95: manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))\nmanagerrepo/prestartup_script.py:150: print(f\"[ComfyUI-Manager] '{pkg}' is remapped to '{res}'\")\nmanagerrepo/prestartup_script.py:454: print(f\"[ComfyUI-Manager] Logging failed: {e}\")\nmanagerrepo/prestartup_script.py:467: print(\"## ComfyUI-Manager: installing dependencies. (GitPython)\")\nmanagerrepo/prestartup_script.py:471: print(\"## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.\")\nmanagerrepo/prestartup_script.py:475: print(\"## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%… [truncated]sed -n '1,140p' managerrepo/prestartup_script.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":11,"exit_code":0,"stderr":"","stdout":"import os\nimport shutil\nimport subprocess\nimport sys\nimport atexit\nimport threading\nimport re\nimport locale\nimport platform\nimport json\nimport ast\nimport logging\nimport traceback\n\nglob_path = os.path.join(os.path.dirname(__file__), \"glob\")\nsys.path.append(glob_path)\n\nimport security_check\nimport manager_util\nimport cm_global\nimport manager_downloader\nimport folder_paths\n\nmanager_util.add_python_path_to_env()\n\nimport datetime as dt\n\nif hasattr(dt, 'datetime'):\n from datetime import datetime as dt_datetime\n \n def current_timestamp():\n return dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]\nelse:\n # NOTE: Occurs in some Mac environments.\n import time\n logging.error(f\"[ComfyUI-Manager] fallback timestamp mode\\n datetime module is invalid: '{dt.__file__}'\")\n \n def current_timestamp():\n return str(time.time()).split('.')[0]\n\n\ncm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}\ncm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torch… [truncated]sed -n '140,400p' managerrepo/prestartup_script.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":11,"exit_code":0,"stderr":"","stdout":" with open(manager_pip_blacklist_path, 'r', encoding=\"UTF-8\", errors=\"ignore\") as f:\n for x in f.readlines():\n y = x.strip()\n if y != '':\n cm_global.pip_blacklist.add(y)\n\n\ndef remap_pip_package(pkg):\n if pkg in cm_global.pip_overrides:\n res = cm_global.pip_overrides[pkg]\n print(f\"[ComfyUI-Manager] '{pkg}' is remapped to '{res}'\")\n return res\n else:\n return pkg\n\n\nstd_log_lock = threading.Lock()\n\n\ndef handle_stream(stream, prefix):\n stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')\n for msg in stream:\n if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):\n if msg.startswith('100%'):\n print('\\r' + msg, end=\"\", file=sys.stderr),\n else:\n print('\\r' + msg[:-1], end=\"\", file=sys.stderr),\n else:\n if prefix == '[!]':\n print(prefix, msg, end=\"\", file=sys.stderr)\n else:\n … [truncated]sed -n '400,1000p' managerrepo/prestartup_script.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":11,"exit_code":0,"stderr":"","stdout":" sys.stdout = ComfyUIManagerLogger(True)\n stderr_wrapper = ComfyUIManagerLogger(False)\n sys.stderr = stderr_wrapper\n\n atexit.register(close_log)\n else:\n sys.stdout.close_log = lambda: None\n stderr_wrapper = None\n\n\n class LoggingHandler(logging.Handler):\n def emit(self, record):\n global is_start_mode\n\n message = record.getMessage()\n\n if is_start_mode:\n match = re.search(pat_import_fail, message)\n if match:\n import_failed_extensions.add(match.group(1).strip())\n\n if 'Traceback' in message:\n file_lists = self._extract_file_paths(message)\n origin_info = extract_origin_module_from_strings(file_lists)\n if origin_info is not None:\n name, origin_path = origin_info\n \n if name != 'comfyui-manager':\n if name not in cm_global.error_dict:\n … [truncated]grep -RIn "routes\..*user" comfyuirepo | sed -n '1,200p'comfyuirepo/app/user_manager.py:125: @routes.get("/users")
comfyuirepo/app/user_manager.py:136: @routes.post("/users")
comfyuirepo/app/user_manager.py:149: @routes.get("/userdata")
comfyuirepo/app/user_manager.py:213: @routes.get("/v2/userdata")
comfyuirepo/app/user_manager.py:333: @routes.get("/userdata/{file}")
comfyuirepo/app/user_manager.py:341: @routes.post("/userdata/{file}")
comfyuirepo/app/user_manager.py:397: @routes.delete("/userdata/{file}")
comfyuirepo/app/user_manager.py:407: @routes.post("/userdata/{file}/move/{dest}")sed -n '1,460p' comfyuirepo/app/user_manager.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":11,"exit_code":0,"stderr":"","stdout":"from __future__ import annotations\nimport json\nimport os\nimport re\nimport uuid\nimport glob\nimport shutil\nimport logging\nfrom aiohttp import web\nfrom urllib import parse\nfrom comfy.cli_args import args\nimport folder_paths\nfrom .app_settings import AppSettings\nfrom typing import TypedDict\n\ndefault_user = \"default\"\n\n\nclass FileInfo(TypedDict):\n path: str\n size: int\n modified: int\n created: int\n\n\ndef get_file_info(path: str, relative_to: str) -> FileInfo:\n return {\n \"path\": os.path.relpath(path, relative_to).replace(os.sep, '/'),\n \"size\": os.path.getsize(path),\n \"modified\": os.path.getmtime(path),\n \"created\": os.path.getctime(path)\n }\n\n\nclass UserManager():\n def __init__(self):\n user_directory = folder_paths.get_user_directory()\n\n self.settings = AppSettings(self)\n if not os.path.exists(user_directory):\n os.makedirs(user_directory, exist_ok=True)\n if not args.multi_user:\n logging.warning(\"****** User settings have … [truncated]sed -n '1,260p' comfyuirepo/app/app_settings.py{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":11,"exit_code":0,"stderr":"","stdout":"import os\nimport json\nfrom aiohttp import web\nimport logging\n\n\nclass AppSettings():\n def __init__(self, user_manager):\n self.user_manager = user_manager\n\n def get_settings(self, request):\n try:\n file = self.user_manager.get_request_user_filepath(\n request,\n \"comfy.settings.json\"\n )\n except KeyError as e:\n logging.error(\"User settings not found.\")\n raise web.HTTPUnauthorized() from e\n if os.path.isfile(file):\n try:\n with open(file) as f:\n return json.load(f)\n except:\n logging.error(f\"The user settings file is corrupted: {file}\")\n return {}\n else:\n return {}\n\n def save_settings(self, request, settings):\n file = self.user_manager.get_request_user_filepath(\n request, \"comfy.settings.json\")\n with open(file, \"w\") as f:\n f.write(json.dumps(settings, indent=4))\n\n def add_routes(self… [truncated]grep -RIn "requirements.txt" comfyuirepo | cat{"cwd":"/root/.pruva/runs/ghsa-2hc9-cc65-xwj8_20260108-081328","duration_ms":146,"exit_code":0,"stderr":"","stdout":"comfyuirepo/tests-unit/README.md:5:`pip install -r tests-unit/requirements.txt`\ncomfyuirepo/README.md:287:```pip install -r requirements.txt```\ncomfyuirepo/README.md:337: pip install -r manager_requirements.txt\ncomfyuirepo/app/frontend_management.py:48: \"\"\"Get the required frontend version from requirements.txt.\"\"\"\ncomfyuirepo/app/frontend_management.py:56: logging.error(f\"Invalid version format in requirements.txt: {version_str}\")\ncomfyuirepo/app/frontend_management.py:59: logging.error(\"comfyui-frontend-package not found in requirements.txt\")\ncomfyuirepo/app/frontend_management.py:62: logging.error(\"requirements.txt not found. Cannot determine required frontend version.\")\ncomfyuirepo/app/frontend_management.py:65: logging.error(f\"Error reading requirements.txt: {e}\")\ncomfyuirepo/app/frontend_management.py:220: \"\"\"Get the required workflow templates version from requirements.txt.\"\"\"\ncomfyuirepo/app/frontend_management.py:228: logging.error(f\"Invalid t… [truncated]git -C managerrepo fetch --unshallow -q && git -C managerrepo rev-list --all --grep="v3.38" --oneline | head -n 5Tool execution failed
git -C managerrepo log --oneline --decorate --graph --all | head -n 200* 8d67702 (grafted, HEAD -> main, origin/main, origin/HEAD) update DB
sed -n '1,200p' comfyuirepo/requirements.txtcomfyui-frontend-package==1.35.9 comfyui-workflow-templates==0.7.69 comfyui-embedded-docs==0.3.1 torch torchsde torchvision torchaudio numpy>=1.25.0
git -C managerrepo fetch --tags --prune --unshallow || git -C managerrepo fetch --tags --prune --depth 1000fatal: --unshallow on a complete repository does not make sense From https://github.com/Comfy-Org/ComfyUI-Manager * [new tag] 3.0.1 -> 3.0.1 * [new tag] 4.0.3 -> 4.0.3 * [new tag] 4.0.4 -> 4.0.4
git -C managerrepo tag --sort=version:refname | tail -n 203.32.4 3.32.5 3.33 3.33.2 3.33.3 3.33.4 3.33.5 3.33.6
git -C managerrepo show -s --format=%H 3.38tag 3.38 Tagger: Dr.Lt.Data <dr.lt.data@gmail.com> v3.38 aaed1dc3d568f012a98c9054acc4450e57e4456c
Artifacts and Evidence for CVE-2025-67303
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available
How to Fix CVE-2025-67303
Upgrade ComfyUI-Manager · pip to 3.38 or later.
FAQ: CVE-2025-67303
How does the CVE-2025-67303 exposure work?
Which ComfyUI-Manager versions are affected by CVE-2025-67303, and where is it fixed?
How severe is CVE-2025-67303?
How can I reproduce CVE-2025-67303?
References for CVE-2025-67303
Authoritative sources for CVE-2025-67303 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.