Defense-in-depth over GET→POST alone: reject the three CORS-safelisted simple-form Content-Types (x-www-form-urlencoded, multipart/form-data, text/plain) on 16 no-body POST handlers (glob + legacy) to block <form method=POST> CSRF that bypasses method-only gating. Move comfyui_switch_version to a JSON body so the preflight requirement applies. Split db_mode/policy/update/channel_url_list into GET(read) + POST(write). Tighten do_fix (high → high+) and gate three previously-ungated config setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations, ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage, and a CHANGELOG. Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/ reinstall/abort_current) are removed; callers already use queue/batch. Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained. Reported-by: XlabAI Team of Tencent Xuanwu Lab CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
21 KiB
Report A — Endpoint Extraction + Scenario Mapping
Generated: 2026-04-18 Source files:
comfyui_manager/glob/manager_server.py(glob v2 — primary/current API)comfyui_manager/legacy/manager_server.py(legacy —--enable-manager-legacy-ui)
Summary
| Category | Endpoints | Unique Scenarios |
|---|---|---|
| Glob v2 | 30 | 120 |
| Legacy-only (not in glob) | 9 | 34 |
| Legacy-shared (same path as glob) | 29 | — (see glob) |
| TOTAL unique HTTP handlers | 39 | 154 |
Security-gated endpoints:
middle: reboot, snapshot/remove, _uninstall_custom_node, _update_custom_nodemiddle+: update_all, snapshot/restore, _install_custom_node, _install_modelhigh+: comfyui_switch_version, install/git_url, install/pip, non-safetensors model install, _fix_custom_node (raised fromhighto align the gate with theSECURITY_MESSAGE_HIGH_Plog text — WI-#235; priormiddle→highupgrade was commitc8992e5d)
Section 1 — Glob v2 Endpoints (comfyui_manager/glob/manager_server.py)
1.1 Queue Management
POST /v2/manager/queue/task
- Handler:
queue_task(L1218) - Schema:
QueueTaskItem(Pydantic) —{kind, ui_id, client_id, params} - Scenarios:
- Success — valid task (kind=install/update/fix/disable/enable/uninstall/update_comfyui/install_model) → 200
- Validation error — malformed kind / missing ui_id / invalid params → 400 with ValidationError text
- Invalid JSON body → 500
- State: worker auto-starts when task added
GET /v2/manager/queue/history_list
- Handler:
get_history_list(L1252) - Scenarios:
- Success — list of batch history file IDs (basename without .json) sorted by mtime desc → 200
{ids: [...]} - Empty history directory → 200
{ids: []} - History path inaccessible → 400
- Success — list of batch history file IDs (basename without .json) sorted by mtime desc → 200
GET /v2/manager/queue/history
- Handler:
get_history(L1281) - Query:
id(batch file) |client_id|ui_id|max_items|offset - Scenarios:
- Success with
id=<batch_history_id>→ reads JSON file → 200 - Path-traversal attempt in
id→ 400 "Invalid history id" - Filter by
ui_id— returns single task history → 200{history: ...} - Filter by
client_id— filters in-memory history dict → 200 - Pagination (
max_items,offset) → 200{history: ...} - JSON serialization failure (in-memory TaskHistoryItem not serializable) → 400
- No query params — returns full current session history → 200
- Success with
POST /v2/manager/queue/reset
- Handler:
reset_queue(L1718) - Scenarios:
- Success — wipes pending + running + history → 200
- Idempotent — safe to call when queue already empty → 200
GET /v2/manager/queue/status
- Handler:
queue_count(L1725) - Query:
client_id(optional) - Scenarios:
- No filter — global counts (total/done/in_progress/pending, is_processing) → 200
- With
client_id— response includesclient_idecho + per-client counts → 200 - Unknown client_id — returns 0 counts with echo → 200
POST /v2/manager/queue/start
- Handler:
queue_start(L1778) - Scenarios:
- Worker not running — starts worker → 200
- Worker already running → 201 (already in-progress)
- Empty queue — worker starts then idles → 200
POST /v2/manager/queue/update_all
- Handler:
update_all(L1411) - Security gate:
middle+→ 403 otherwise - Schema:
UpdateAllQueryParams—{ui_id, client_id, mode?} - Scenarios:
- Success — queues update tasks for all active nodes → 200 (synchronously; slow due to reload)
- Missing
ui_id/client_id→ 400 ValidationError - Security blocked (level < middle+) → 403
mode=local— uses local channel (no network)mode=remote/cache— fetches cached channel data- Desktop version — skips comfyui-manager pack (reads
__COMFYUI_DESKTOP_VERSION__) - Empty node set — queues 0 tasks → 200
POST /v2/manager/queue/update_comfyui
- Handler:
update_comfyui(L1791) - Schema:
UpdateComfyUIQueryParams—{client_id, ui_id, stable?} - Scenarios:
- Success — queues
update-comfyuitask → 200 - Missing required params → 400 ValidationError
stable=true— forces stable update regardless of configstable=false— forces nightly update- No
stableparam — uses config policy (nightly-comfyui vs stable)
- Success — queues
POST /v2/manager/queue/install_model
- Handler:
install_model(L1875) - Schema:
ModelMetadata(name, type, base, url, filename, save_path) + requiredclient_id+ui_id - Scenarios:
- Success — valid request → queues
install-modeltask → 200 - Missing
client_id→ 400 "Missing required field: client_id" - Missing
ui_id→ 400 "Missing required field: ui_id" - Invalid model metadata (missing name/url/filename) → 400 ValidationError
- Malformed JSON → 500
- Success — valid request → queues
1.2 Custom Node Info
GET /v2/customnode/getmappings
- Handler:
fetch_customnode_mappings(L1357) - Query:
mode(required: local|cache|remote|nickname) - Scenarios:
- Success — returns
{pack_id: [node_list, metadata]}dict → 200 mode=nickname— applies nickname_filter → 200- Missing
mode→ KeyError → 500 - Invalid
modevalue → may raise from get_data_by_mode → 400/500 - Missing nodes matched by
nodename_patternregex are appended
- Success — returns
GET /v2/customnode/fetch_updates
- Handler:
fetch_updates(L1393) - Scenarios:
- Always returns 410 Gone with
{deprecated: true}— deprecated endpoint - Client should migrate to queue/update-based flow
- Always returns 410 Gone with
GET /v2/customnode/installed
- Handler:
installed_list(L1500) - Query:
mode(default|imported) - Scenarios:
- Default mode — current installed packs snapshot → 200 dict
mode=imported— startup-time installed packs (frozen snapshot) → 200- Empty custom_nodes dir → 200
{}
POST /v2/customnode/import_fail_info
- Handler:
import_fail_info(L1623) - Body:
{cnr_id?, url?}— one required - Scenarios:
- Known failed pack via
cnr_id— returns{msg, traceback}→ 200 - Known failed pack via
url— returns info → 200 - Unknown pack → 400 (no failure info available)
- Missing both cnr_id and url → 400 "Either 'cnr_id' or 'url' field is required"
cnr_idnot a string → 400 "'cnr_id' must be a string"- Non-dict body → 400 "Request body must be a JSON object"
- Known failed pack via
POST /v2/customnode/import_fail_info_bulk
- Handler:
import_fail_info_bulk(L1657) - Schema:
ImportFailInfoBulkRequest—{cnr_ids: [], urls: []} - Scenarios:
- Success with
cnr_idslist — returns{cnr_id: {error, traceback}|null}→ 200 - Success with
urlslist — returns{url: {error, traceback}|null}→ 200 - Both lists empty → 400 "Either 'cnr_ids' or 'urls' field is required"
- Validation error (wrong types) → 400
- Each unknown pack →
nullin results dict (not an error)
- Success with
1.3 Snapshots
GET /v2/snapshot/getlist
- Handler:
get_snapshot_list(L1512) - Scenarios:
- Success — list of snapshot file stems (basename minus .json), sorted desc → 200
{items: [...]} - Empty snapshot dir → 200
{items: []}
- Success — list of snapshot file stems (basename minus .json), sorted desc → 200
GET /v2/snapshot/get_current
- Handler:
get_current_snapshot_api(L1575) - Scenarios:
- Success — returns current system state dict → 200
- Internal failure → 400
POST /v2/snapshot/save
- Handler:
save_snapshot(L1585) - Scenarios:
- Success — creates timestamped snapshot file → 200
- Internal failure → 400
- Multiple rapid saves — each creates distinct timestamped file
POST /v2/snapshot/remove
- Handler:
remove_snapshot(L1521) - Security gate:
middle→ 403 - Query:
target(snapshot file stem) - Scenarios:
- Success — removes existing file → 200
- Nonexistent target — 200 (no-op)
- Path traversal (
../x) → 400 "Invalid target" - Missing
targetquery → exception → 400 - Security blocked (level < middle) → 403
POST /v2/snapshot/restore
- Handler:
restore_snapshot(L1543) - Security gate:
middle+→ 403 - Query:
target - Scenarios:
- Success — copies snapshot to startup script path (applied on next reboot) → 200
- Nonexistent target → 400
- Path traversal → 400 "Invalid target"
- Security blocked → 403
1.4 Configuration
GET /v2/manager/db_mode
- Handler:
db_mode(L1907) - Scenarios: Returns plain text current value ∈ {cache, channel, local, remote} → 200
POST /v2/manager/db_mode
- Handler:
set_db_mode_api(L1912) - Body:
{value: <mode>} - Scenarios:
- Valid value — persists to config.ini → 200
- Malformed JSON → 400 "Invalid request"
- Missing
valuekey → 400 KeyError
GET /v2/manager/policy/update
- Handler:
update_policy(L1923) - Scenarios: Returns plain text value ∈ {stable, stable-comfyui, nightly, nightly-comfyui} → 200
POST /v2/manager/policy/update
- Handler:
set_update_policy_api(L1928) - Body:
{value: <policy>} - Scenarios:
- Valid value → persists config → 200
- Malformed JSON / missing value → 400
GET /v2/manager/channel_url_list
- Handler:
channel_url_list(L1939) - Scenarios:
- Success —
{selected: name, list: ["name::url", ...]}→ 200 - Selected URL doesn't match any channel →
selected="custom"
- Success —
POST /v2/manager/channel_url_list
- Handler:
set_channel_url(L1954) - Body:
{value: <channel_name>} - Scenarios:
- Known channel name → persists new URL → 200
- Unknown channel name → no-op → 200 (silent)
- Malformed JSON / missing value → 400
1.5 System
GET /v2/manager/is_legacy_manager_ui
- Handler:
is_legacy_manager_ui(L1487) - Scenarios: Returns
{is_legacy_manager_ui: bool}reflecting--enable-manager-legacy-uiflag → 200
GET /v2/manager/version
- Handler:
get_version(L2009) - Scenarios: Returns plain text
core.version_str→ 200
POST /v2/manager/reboot
- Handler:
restart(L1968) - Security gate:
middle→ 403 - Scenarios:
- Success — triggers server process restart via execv → 200 (connection may drop)
__COMFY_CLI_SESSION__env set — writes reboot marker + exit(0) instead of execv- Desktop/Windows standalone variant — removes flag before restart
- Security blocked → 403
1.6 ComfyUI Version Management
GET /v2/comfyui_manager/comfyui_versions
- Handler:
comfyui_versions(L1825) - Scenarios:
- Success —
{versions: [...], current: "<tag or hash>"}→ 200 - Git access failure → 400
- Success —
POST /v2/comfyui_manager/comfyui_switch_version
- Handler:
comfyui_switch_version(L1840) - Security gate:
high+→ 403 - Schema:
ComfyUISwitchVersionParams—{ver, client_id, ui_id}(JSON body; renamed in WI #261, migrated from query string in WI #258) - Scenarios:
- Success — queues update-comfyui task with target_version → 200
- Missing
ver/client_id/ui_id→ 400 ValidationError (JSON witherrorfield) - Security blocked → 403
- Internal exception → 400
Section 2 — Legacy-Only Endpoints (comfyui_manager/legacy/manager_server.py)
Endpoints in this section exist only in the legacy server (not registered in glob). They are served when --enable-manager-legacy-ui is set.
POST /v2/manager/queue/batch
- Handler:
queue_batch(L740) - Body: dict with keys ∈ {update_all, reinstall, install, uninstall, update, update_comfyui, disable, install_model, fix}, each with a list of per-pack payloads
- Scenarios:
- Success with single kind (e.g. install) → appends to temp_queue_batch, finalizes, starts worker → 200
{failed: []} - Mixed kinds in one batch — processes each sequentially
- Partial failure — some packs fail internally → 200
{failed: [...ids]} update_allwith mode — runs legacy update_all flow inlinereinstall— uninstall then install per pack; uninstall failure skips installdisable— no security check inside_disable_nodeinstallwith security gate fail → failed set entry- Empty body
{}→ 200{failed: []}
- Success with single kind (e.g. install) → appends to temp_queue_batch, finalizes, starts worker → 200
GET /v2/customnode/getlist
- Handler:
fetch_customnode_list(L1018) - Query:
mode(required),skip_update?(bool string) - Scenarios:
- Success — returns
{channel, node_packs}with installed/update state populated → 200 skip_update=true— skips git update check (faster)- Removes comfyui-manager self-entry from results
- Channel lookup resolves to 'default'/'custom'/known name
- Success — returns
GET /customnode/alternatives
- Handler:
fetch_customnode_alternatives(L1072) - Query:
mode(required) - Scenarios:
- Success — alter-list.json items keyed by id → 200
GET /v2/externalmodel/getlist
- Handler:
fetch_externalmodel_list(L1143) - Query:
mode(required) - Scenarios:
- Success — model-list.json with
installedflag populated per file → 200 - HuggingFace sentinel filename — resolves from URL basename
- Custom save_path — checks under models/<save_path>
- Success — model-list.json with
GET /v2/customnode/versions/{node_name}
- Handler:
get_cnr_versions(L1262) - Path param:
node_name - Scenarios:
- Known CNR pack — returns version list → 200
- Unknown pack — 400
GET /v2/customnode/disabled_versions/{node_name}
- Handler:
get_disabled_versions(L1273) - Scenarios:
- Pack has nightly_inactive entry → version list includes "nightly"
- Pack has cnr_inactive entries → versions list
- No disabled versions → 400
POST /v2/customnode/install/git_url
- Handler:
install_custom_node_git_url(L1502) - Security gate:
high+→ 403 - Body: plain text URL
- Scenarios:
- Success install → 200
- Already installed (skip action) → 200
- Clone failure → 400
- Security blocked → 403
POST /v2/customnode/install/pip
- Handler:
install_custom_node_pip(L1522) - Security gate:
high+→ 403 - Body: plain text space-separated packages
- Scenarios:
- Success — pip install completes → 200
- Security blocked → 403
GET /v2/manager/notice
- Handler:
get_notice(L1747) - Scenarios:
- Success — fetches GitHub wiki News, returns HTML → 200
- GitHub unreachable / non-200 → 200 "Unable to retrieve Notice" (plain text)
- No markdown-body div matched → 200 "Unable to retrieve Notice"
- Appends ComfyUI/Manager version footer
- Desktop variant — uses
__COMFYUI_DESKTOP_VERSION__ - Non-git ComfyUI — prepends "Your ComfyUI isn't git repo" warning
- Outdated ComfyUI (required_commit_datetime > current) — prepends "too OUTDATED" warning
Section 3 — Legacy-Shared Endpoints
These paths exist in BOTH glob and legacy files (29 endpoints). Semantics are typically equivalent but implementation may differ; see glob scenarios above. Notable differences:
- queue/status (L1379 legacy) — counts from
task_batch_queue[0](first batch only), not aggregated across batches like glob - queue/start (L1465 legacy) — calls
finalize_temp_queue_batch()first, then starts worker thread - update_all (L904 legacy) — returns 401 if worker already running; auto-saves snapshot; uses temp_queue_batch instead of QueueTaskItem
- update_comfyui (L1572 legacy) — no params; reads config.update_policy directly; always returns 200
- reboot (L1796 legacy) — identical behavior to glob
- history (L819 legacy) — only supports
idquery (no client_id/ui_id/pagination) - import_fail_info (L1289 legacy) — no basic dict validation; assumes cnr_id/url present (KeyError otherwise)
Security Level Matrix
| Level | Glob endpoints | Legacy endpoints |
|---|---|---|
| middle | snapshot/remove, reboot | snapshot/remove, reboot, _uninstall, _update |
| middle+ | update_all, snapshot/restore, install_model | update_all, snapshot/restore, _install_custom_node, _install_model |
| high+ | comfyui_switch_version, _fix_custom_node | comfyui_switch_version, install/git_url, install/pip, non-safetensors model install, _fix |
Note:
_fix/_fix_custom_nodesecurity-level history:middle→highin commitc8992e5d(2026-04-04, which also added a previously-missing gate to the legacy handler); subsequenthigh→high+in WI-#235 to align the enforcement gate with theSECURITY_MESSAGE_HIGH_Plog text (and tighten the gate for a state-mutating fix path). README 'Risky Level Table' has been updated in lockstep.
Deprecated / Removed
- GET /v2/customnode/fetch_updates — glob returns 410; legacy still attempts fetch (may succeed but deprecated in concept)
- Individual queue/{install,uninstall,update,fix,disable,reinstall,abort_current} — removed from legacy in recent work (replaced by queue/batch aggregator)
- GET /manager/notice (v1, no /v2 prefix) — removed from legacy
CSRF Method-Reject Contract Inventory
Purpose: Enumerate the 16 state-changing endpoints that must reject HTTP GET after commit 99caef55 (CSRF method-conversion mitigation; CVSS 8.1, reported by XlabAI / Tencent Xuanwu). This inventory supplements verification_design.md Section 10 (Goals CSRF-M1 / M2 / M3) and is the authoritative cross-reference for the contract enforced by tests/e2e/test_e2e_csrf.py.
Data Source: tests/e2e/test_e2e_csrf.py::STATE_CHANGING_POST_ENDPOINTS (L92-L109). Pre-99caef55 methods derived from git log -S history and the commit body of 99caef55. Security Level column cross-references the Security Level Matrix (§ above, L378-L382).
| # | Endpoint | Pre-99caef55 Method | Post-99caef55 Method | Security Level | Test Reference |
|---|---|---|---|---|---|
| 1 | /v2/manager/queue/start |
GET | POST | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/start] |
| 2 | /v2/manager/queue/reset |
GET | POST | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/reset] |
| 3 | /v2/manager/queue/update_all |
GET | POST | middle+ | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/update_all] |
| 4 | /v2/manager/queue/update_comfyui |
GET | POST | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/update_comfyui] |
| 5 | /v2/manager/queue/install_model |
POST | POST (pre-existing) | middle+ | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/install_model] |
| 6 | /v2/manager/queue/task |
POST | POST (pre-existing) | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/queue/task] |
| 7 | /v2/snapshot/save |
GET | POST | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/snapshot/save] |
| 8 | /v2/snapshot/remove |
GET | POST | middle | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/snapshot/remove] |
| 9 | /v2/snapshot/restore |
GET | POST | middle+ | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/snapshot/restore] |
| 10 | /v2/manager/reboot |
GET | POST | middle | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/reboot] |
| 11 | /v2/comfyui_manager/comfyui_switch_version |
GET | POST | high+ | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/comfyui_manager/comfyui_switch_version] |
| 12 | /v2/manager/db_mode |
GET (dual) | POST (write); GET preserved for read | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/db_mode] |
| 13 | /v2/manager/policy/update |
GET (dual) | POST (write); GET preserved for read | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/policy/update] |
| 14 | /v2/manager/channel_url_list |
GET (dual) | POST (write); GET preserved for read | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/manager/channel_url_list] |
| 15 | /v2/customnode/import_fail_info |
POST | POST (pre-existing) | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/customnode/import_fail_info] |
| 16 | /v2/customnode/import_fail_info_bulk |
POST | POST (pre-existing) | default | TestStateChangingEndpointsRejectGet::test_get_is_rejected[/v2/customnode/import_fail_info_bulk] |
Conversion Breakdown (reconciles commit 99caef55 body with 16-row fixture):
- Pure GET → POST conversions: 9 endpoints (rows 1-4, 7-11) — confirmed write-only operations formerly exposed via GET.
- Dual-method endpoints (GET + POST coexist after fix): 3 endpoints (rows 12-14) — the POST variant carries write semantics; GET preserved for read-only retrieval and is covered by Goal CSRF-M3.
- Pre-existing POST endpoints (included in the fixture for contract completeness): 4 endpoints (rows 5-6, 15-16) — these were already POST before 99caef55 but remain part of the CSRF rejection contract so any future regression to GET is caught.
Scope Note: This inventory narrowly documents the method-restriction layer. Complementary CSRF defenses (Origin/Referer validation, same-site cookies, anti-CSRF tokens, cross-site form POST rejection) are out of scope for this contract and tracked in verification_design.md § 10.2.
End of Report A