ComfyUI-Manager/reports/verification_design.md
Dr.Lt.Data 4410ebc6a6
Some checks are pending
Publish to PyPI / build-and-publish (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
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)
2026-04-22 05:04:30 +09:00

38 KiB
Raw Blame History

Verification Design — Per-Goal Verification Items

Generated: 2026-04-18 Source: Derived from endpoint_scenarios.md, scenario_intents.md, scenario_effects.md Purpose: For each scenario's achievement goal, design the concrete verification items (assertions + observables + tools) required to prove the goal is met.

Verification item format (used throughout):

Goal: <what scenario intends to achieve>
Precondition: <state required before the action>
Action: <call / UI interaction>
Observable: <what can be inspected>
Assertion: <pass/fail criterion>
Negative check: <what must NOT happen>

Section 1 — Queue Management Verification Design

1.1 POST /v2/manager/queue/task (kind=install)

Goal A1 — Install a CNR pack to loadable state

  • Precondition: pack dir absent; .tracking absent
  • Action: POST queue/task kind=install params={id, version, selected_version, mode, channel} → POST queue/start
  • Observable: filesystem custom_nodes/<pack>/ + .tracking file content; customnode/installed response; WebSocket cm-queue-status all-done
  • Assertion: pack dir exists AND .tracking present AND installed[pack_id].cnr_id == pack_id AND version matches
  • Negative check: no leftover .trash_* dirs; no ModuleNotFoundError in server log

Goal A2 — Install a nightly (URL) pack via git clone

  • Precondition: pack dir absent
  • Action: POST queue/task kind=install selected_version=nightly with repository URL
  • Observable: pack dir + .git/ subdir; .git/config remote URL
  • Assertion: .git exists AND remote URL matches request
  • Negative check: no ModuleNotFoundError (git_helper.py subprocess ran cleanly)

Goal A3 — Skip install when already disabled (re-enable shortcut)

  • Precondition: pack in .disabled/
  • Action: POST queue/task kind=install params.skip_post_install=true for known cnr_id
  • Observable: pack moved back to active custom_nodes/; no fresh clone
  • Assertion: pack active + no new download; existing .tracking preserved
  • Negative check: no re-download (verify via timing or lack of network log)

Goal A4 — Reject bad kind (validation error)

  • Precondition: queue empty or known state
  • Action: POST queue/task with kind="garbage"
  • Observable: response status; queue/status.total_count before/after
  • Assertion: 400 + ValidationError text; total_count unchanged
  • Negative check: no pack changes; no task in history

Goal A5 — Reject missing ui_id/client_id (traceability gate)

  • Action: POST queue/task without ui_id OR without client_id
  • Assertion: 400; no task queued (verify via status counts)

Goal A6 — Worker auto-starts on task queue

  • Precondition: worker idle
  • Action: POST queue/task (any valid)
  • Observable: queue/status.is_processing polling
  • Assertion: within N seconds is_processing becomes true; eventually done_count increments

1.2 POST /v2/manager/queue/task (kind=uninstall)

Goal U1 — Remove installed pack

  • Precondition: pack present; in installed list
  • Action: POST queue/task kind=uninstall params.node_name=<cnr_id>
  • Observable: filesystem pack dir; customnode/installed
  • Assertion: pack dir absent AND cnr_id absent from installed
  • Negative check: other packs untouched; no orphan files in .disabled/

Goal U2 — Idempotent uninstall of missing pack

  • Precondition: pack absent
  • Action: POST queue/task kind=uninstall for non-existent pack
  • Assertion: task completes without raising fatal error; state unchanged

1.3 POST /v2/manager/queue/task (kind=update)

Goal UP1 — Upgrade pack to newer version

  • Precondition: pack installed at version X; version Y > X available
  • Action: POST queue/task kind=update params.node_name, node_ver
  • Observable: .tracking file content (version field); CNR API version
  • Assertion: pack still present; new version recorded; dependencies updated
  • Negative check: no partial state (no half-cloned dir)

Goal UP2 — Idempotent when already up-to-date

  • Precondition: pack at latest version
  • Action: POST queue/task kind=update
  • Assertion: no version downgrade; no pack removal; task completes

1.4 POST /v2/manager/queue/task (kind=fix)

Goal F1 — Reinstall dependencies of existing pack

  • Precondition: pack present; dependencies broken (simulate by removing from venv)
  • Action: POST queue/task kind=fix params.node_name, node_ver
  • Observable: venv site-packages for pack's requirements; import succeeds
  • Assertion: after fix, all declared requirements importable; pack dir unchanged (same HEAD)

1.5 POST /v2/manager/queue/task (kind=disable)

Goal D1 — Move pack to disabled state reversibly

  • Precondition: pack active
  • Action: POST queue/task kind=disable params.node_name, is_unknown
  • Observable: custom_nodes/.disabled/<pack>/ exists; custom_nodes/<pack>/ absent
  • Assertion: pack relocated to .disabled/; not in active installed list; on reload NODE_CLASS_MAPPINGS lacks its nodes
  • Negative check: pack files preserved (not deleted)

Goal D2 — Idempotent disable of already-disabled pack

  • Precondition: pack in .disabled/
  • Action: POST queue/task kind=disable
  • Assertion: pack still in .disabled/; no duplicate; no error

1.6 POST /v2/manager/queue/task (kind=enable)

Goal E1 — Restore pack from .disabled/ to active

  • Precondition: pack in .disabled/
  • Action: POST queue/task kind=enable params.cnr_id
  • Observable: custom_nodes/<pack>/ exists (may be lowercase variant); .disabled/ entry gone
  • Assertion: pack active; appears in installed; on next reload nodes load
  • Negative check: no duplicate entries

1.7 POST /v2/manager/queue/install_model

Goal IM1 — Download model to correct subdirectory

  • Precondition: model file absent; URL in whitelist
  • Action: POST queue/install_model with valid ModelMetadata + client_id + ui_id
  • Observable: models/<type>/<filename> file; file size > 0
  • Assertion: file exists + non-zero size; externalmodel/getlist shows installed=True
  • Negative check: no orphan partial downloads

Goal IM2 — Reject missing client_id / ui_id

  • Action: POST install_model without one of them
  • Assertion: 400; no task queued; no download attempted

Goal IM3 — Reject non-whitelist URL (legacy)

  • Action: POST install_model with URL NOT in whitelist
  • Assertion: 400; no file written

Goal IM4 — Block non-safetensors below high+ security

  • Precondition: security_level < high+
  • Action: POST install_model with filename="*.ckpt"
  • Assertion: 403; no file written

1.8 POST /v2/manager/queue/update_all

Goal UA1 — Queue update tasks for all active packs

  • Precondition: N active packs installed; queue empty
  • Action: POST queue/update_all with ui_id, client_id, mode
  • Observable: queue/status.pending_count delta
  • Assertion: pending_count increased by N (minus manager-skip if desktop); each queued task has kind=update + correct node_name
  • Negative check: comfyui-manager NOT in queue if COMFYUI_DESKTOP_VERSION set

Goal UA2 — Security gate (<middle+)

  • Precondition: security_level < middle+
  • Action: POST queue/update_all
  • Assertion: 403; no tasks queued

Goal UA3 — Require ui_id + client_id

  • Action: POST queue/update_all without required params
  • Assertion: 400 ValidationError; no queue change

1.9 POST /v2/manager/queue/update_comfyui

Goal UC1 — Queue ComfyUI self-update task

  • Action: POST queue/update_comfyui with client_id, ui_id
  • Observable: queue/status.total_count; queued task's params
  • Assertion: total_count += 1; queued task has kind=update-comfyui with is_stable matching config or override
  • Negative check: actual git operation doesn't start (task queued only)

Goal UC2 — Explicit stable flag override

  • Action: POST queue/update_comfyui?stable=true (config says nightly)
  • Assertion: queued task.params.is_stable == true

1.10 POST /v2/manager/queue/reset

Goal R1 — Clear all queued/running/history tasks

  • Precondition: queue has N tasks
  • Action: POST queue/reset
  • Observable: queue/status all counts
  • Assertion: total_count=done_count=in_progress_count=pending_count=0; is_processing=false
  • Negative check: history tasks also cleared

Goal R2 — Idempotent on empty queue

  • Precondition: queue empty
  • Action: POST queue/reset (2 times)
  • Assertion: 200 both times; state still empty

1.11 POST /v2/manager/queue/start

Goal S1 — Start worker when idle

  • Precondition: is_processing=false; queue has tasks
  • Action: POST queue/start
  • Assertion: 200; is_processing=true; tasks begin processing (done_count eventually increments)

Goal S2 — Don't spawn duplicate worker

  • Precondition: is_processing=true
  • Action: POST queue/start
  • Assertion: 201; still is_processing=true; same worker pid (observable via logs)
  • Negative check: no duplicate task processing (each task runs once)

1.12 GET /v2/manager/queue/status

Goal QS1 — Accurate overall counts

  • Precondition: known queue state (e.g., 3 pending, 1 running, 2 done)
  • Action: GET queue/status
  • Assertion: counts match actual internal queue state

Goal QS2 — Client-filtered counts

  • Precondition: tasks from multiple client_ids
  • Action: GET queue/status?client_id=X
  • Assertion: response.client_id == X; counts reflect only X's tasks

1.13 GET /v2/manager/queue/history

Goal QH1 — Retrieve batch history by id

  • Precondition: batch file exists at manager_batch_history_path
  • Action: GET queue/history?id=<batch_id>
  • Assertion: response JSON matches file contents

Goal QH2 — Reject path traversal

  • Action: GET queue/history?id=../../../etc/passwd
  • Assertion: 400 "Invalid history id"; no file read attempt

Goal QH3 — Filter by ui_id / client_id / pagination

  • Action: GET queue/history with respective query params
  • Assertion: results properly filtered/paginated

1.14 GET /v2/manager/queue/history_list

Goal QHL1 — List batch IDs sorted by mtime

  • Precondition: files exist in history dir
  • Action: GET queue/history_list
  • Assertion: ids list == filenames (stem) sorted by mtime desc

Goal QHL2 — Empty list when no history

  • Precondition: empty dir
  • Assertion: {ids: []}; 200

Section 2 — CustomNode Info Verification Design

2.1 GET /v2/customnode/getmappings

Goal CM1 — Return comprehensive node→pack mapping

  • Action: GET getmappings?mode=local (or cache/remote)
  • Observable: response dict; current NODE_CLASS_MAPPINGS
  • Assertion: every currently-loaded node appears in some entry's node_list OR matches a nodename_pattern regex
  • Negative check: no stale entries for uninstalled packs

Goal CM2 — Nickname mode applies filter

  • Action: GET getmappings?mode=nickname
  • Assertion: entries include nickname field filtered by nickname_filter rules

Goal CM3 — Require explicit mode

  • Action: GET getmappings (no mode param)
  • Assertion: server error (500/KeyError); no partial data

2.2 GET /v2/customnode/fetch_updates (deprecated)

Goal FU1 — Signal deprecation to clients

  • Action: GET fetch_updates?mode=local
  • Assertion: 410 status; body {deprecated: true, error: ..., message: ...}
  • Negative check: no git fetch executed (no mtime changes on .git/)

2.3 GET /v2/customnode/installed

Goal IL1 — Current installed packs

  • Precondition: known packs installed
  • Action: GET installed
  • Assertion: response dict keys include all installed pack identifiers; each entry has cnr_id + version + enabled

Goal IL2 — imported mode is startup-frozen

  • Precondition: install a new pack post-startup
  • Action: GET installed?mode=imported
  • Assertion: new pack absent from response (startup snapshot unchanged)
  • Negative check: default mode DOES show new pack (proves they differ)

2.4 POST /v2/customnode/import_fail_info

Goal IF1 — Return failure info for failed pack

  • Precondition: pack exists in cm_global.error_dict
  • Action: POST import_fail_info {cnr_id}
  • Assertion: 200; response has msg + traceback; content matches error_dict entry

Goal IF2 — Reject unknown pack

  • Action: POST import_fail_info {cnr_id: "unknown-12345"}
  • Assertion: 400; no response body with info

Goal IF3 — Validate request body shape

  • Action: POST import_fail_info with non-dict OR missing both fields OR wrong type
  • Assertion: 400 with specific error text

2.5 POST /v2/customnode/import_fail_info_bulk

Goal IFB1 — Return per-pack lookup results

  • Action: POST import_fail_info_bulk {cnr_ids: [known, unknown]}
  • Assertion: 200; response maps known→info dict, unknown→null

Goal IFB2 — Reject empty lists

  • Action: POST with {cnr_ids:[], urls:[]}
  • Assertion: 400 "Either 'cnr_ids' or 'urls' field is required"

Section 3 — Snapshot Verification Design

3.1 GET /v2/snapshot/get_current

Goal SG1 — Capture current state accurately

  • Precondition: known set of packs installed
  • Action: GET snapshot/get_current
  • Assertion: response has comfyui (hash/tag), git_custom_nodes (list), cnr_custom_nodes (list), pips; entries match actual installed state

3.2 POST /v2/snapshot/save

Goal SS1 — Persist current state to retrievable file

  • Precondition: snapshot dir exists
  • Action: POST snapshot/save
  • Observable: new file in manager_snapshot_path; snapshot/getlist response
  • Assertion: new timestamped file created; content matches get_current() at save time; appears in getlist.items
  • Negative check: other snapshots untouched

Goal SS2 — Multiple saves create distinct files

  • Action: POST save; wait 1s; POST save
  • Assertion: 2 distinct new entries in getlist

3.3 GET /v2/snapshot/getlist

Goal SL1 — List matches filesystem

  • Action: GET snapshot/getlist
  • Assertion: items == .json files in snapshot dir (stems), desc sorted

3.4 POST /v2/snapshot/remove

Goal SR1 — Delete snapshot permanently

  • Precondition: target snapshot exists
  • Action: POST snapshot/remove?target=
  • Assertion: file removed from disk; absent from getlist
  • Negative check: other snapshots untouched

Goal SR2 — Idempotent on missing target

  • Action: POST remove?target=nonexistent
  • Assertion: 200 no-op

Goal SR3 — Reject path traversal

  • Action: POST remove?target=../../etc/passwd
  • Assertion: 400 "Invalid target"; no file ops outside snapshot dir

Goal SR4 — Security gate (<middle)

  • Precondition: security_level < middle
  • Action: POST remove
  • Assertion: 403; target file untouched
  • Test reference: tests/e2e/test_e2e_secgate_strict.py::TestSecurityGate403_SR4::test_remove_returns_403 (WI-KK PoC, audit-integrated by WI-LL — uses start_comfyui_strict.sh strict-mode fixture)

3.5 POST /v2/snapshot/restore

Goal SR5 — Schedule snapshot restore for next reboot

  • Precondition: target snapshot exists
  • Action: POST restore?target=
  • Observable: manager_startup_script_path for restore-snapshot.json
  • Assertion: restore-snapshot.json exists; content == target snapshot; (downstream: reboot → state reverts)

Goal SR6 — Security gate (<middle+)

  • Assertion: 403; no marker file

Section 4 — Config Verification Design

4.1 GET /v2/manager/db_mode

Goal C1 — Return current config value

  • Action: GET db_mode
  • Assertion: text response ∈ {cache, channel, local, remote}; matches config.ini[db_mode]

4.2 POST /v2/manager/db_mode

Goal C2 — Persist new value to config.ini

  • Precondition: original value X
  • Action: POST db_mode {value: Y} where Y ≠ X
  • Observable: GET db_mode after POST; config.ini file content; GET after process restart
  • Assertion: GET returns Y; config.ini updated; survives restart (restart + re-GET returns Y)
  • Cleanup: restore X

Goal C3 — Reject malformed JSON / missing value

  • Action: POST with non-JSON body OR {foo: bar}
  • Assertion: 400; config.ini unchanged

4.3-4.4 GET/POST /v2/manager/policy/update

Same verification pattern as db_mode, with policy values ∈ {stable, stable-comfyui, nightly, nightly-comfyui}.

4.5 GET /v2/manager/channel_url_list

Goal C4 — Return selected + available channels

  • Action: GET channel_url_list
  • Assertion: response has selected (str) + list (array of "name::url" strings); selected == name whose URL matches config.channel_url else "custom"

4.6 POST /v2/manager/channel_url_list

Goal C5 — Switch channel by name

  • Precondition: original channel X
  • Action: POST {value: Y} where Y is a known channel name
  • Observable: GET after POST
  • Assertion: selected == Y; config.channel_url == channels[Y]
  • Cleanup: restore X

Goal C6 — Silent no-op on unknown name

  • Action: POST {value: "nonexistent-channel-xyz"}
  • Assertion: 200; selected UNCHANGED (verify via GET)

Section 5 — System + ComfyUI Version Verification Design

5.1 GET /v2/manager/version

Goal V1 — Consistent version string

  • Action: GET version × 2 consecutive calls
  • Assertion: both return same non-empty string == core.version_str

5.2 GET /v2/manager/is_legacy_manager_ui

Goal V2 — Reflect CLI flag

  • Precondition: server started with/without --enable-manager-legacy-ui
  • Action: GET is_legacy_manager_ui
  • Assertion: response.is_legacy_manager_ui matches the actual CLI flag

5.3 POST /v2/manager/reboot

Goal V3 — Process restart + recovery

  • Precondition: server running; pre_version captured
  • Action: POST reboot
  • Observable: connection behavior; health endpoint polling; post-reboot version
  • Assertion: connection drops or 200; within N seconds server responds again; post_version == pre_version (verifies core state preserved)
  • Negative check: config unchanged across reboot

Goal V4 — COMFY_CLI_SESSION mode

  • Precondition: COMFY_CLI_SESSION env set
  • Action: POST reboot
  • Assertion: .reboot marker file created; process exits with code 0 (external manager restarts)

Goal V5 — Security gate (<middle)

  • Action: POST reboot at low security
  • Assertion: 403; server continues (no restart occurs)

5.4 GET /v2/comfyui_manager/comfyui_versions

Goal CV1 — Enumerate versions + current

  • Action: GET comfyui_versions
  • Assertion: response {versions, current}; versions list non-empty; each item is string; current ∈ versions; matches actual .git tags/commits

Goal CV2 — Fail cleanly on non-git

  • Precondition: ComfyUI dir not a git repo (simulate)
  • Action: GET
  • Assertion: 400; no partial response

5.5 POST /v2/comfyui_manager/comfyui_switch_version

Goal CV3 — Queue update-comfyui task with target_version

  • Precondition: security_level ≥ high+
  • Action: POST switch_version?ver=X&client_id=Y&ui_id=Z
  • Observable: queue/status; queued task params
  • Assertion: task queued with params.target_version=X
  • Note: actual switch (destructive) NOT verified in E2E

Goal CV4 — Security gate (<high+)

  • Action: POST switch_version at lower security
  • Assertion: 403; no task queued
  • Test reference: tests/e2e/test_e2e_secgate_default.py::TestSecurityGate403_CV4::test_switch_version_returns_403_at_default (WI-KK demo, audit-integrated by WI-LL — runs at default security_level=normal; WI-KK research showed is_local_mode=True + normal is already outside the [WEAK, NORMAL_] allowed set for high+, so no harness is needed)

Goal CV5 — Validate params

  • Action: POST without ver OR without client_id/ui_id
  • Assertion: 400 with error details; no task queued

Section 6 — Legacy-only Verification Design (UI → effect)

All verification here assumes Playwright UI interaction as the Action. Effect is observed through both UI state and backend filesystem/state.

6.1 POST /v2/manager/queue/batch

Goal LB1 — Install via UI row "Install" button

  • Precondition: pack not installed; Custom Nodes Manager dialog open
  • Action (UI): filter to Not Installed → click Install on target row → click "Select" on version dialog
  • Observable: backend: custom_nodes// + .tracking; UI: row badge changes to "Installed"; WebSocket: cm-queue-status all-done
  • Assertion: backend install effect + UI state updates

Goal LB2 — Uninstall via UI

  • Precondition: pack installed
  • Action (UI): click "Uninstall" button on row
  • Observable: backend: pack dir removed; UI: row shows "Not Installed"
  • Assertion: both states consistent

Goal LB3 — Disable via UI

  • Action (UI): click "Disable" button on row
  • Assertion: pack in .disabled/; UI row shows "Disabled"

Goal LB4 — Update all via menu

  • Action (UI): Manager menu → "Update All"
  • Observable: queue/status; progress indicator in UI
  • Assertion: N tasks queued; UI progress reflects N; eventual completion

Goal LB5 — Batch partial failure reporting

  • Action (UI): batch with one guaranteed-fail pack
  • Observable: response.failed array; UI notification
  • Assertion: failed list contains only failed pack id; others succeeded

6.2 GET /v2/customnode/getlist

Goal LG1 — Populate Custom Nodes Manager dialog

  • Action (UI): click "Custom Nodes Manager" from Manager menu
  • Observable: UI grid rows
  • Assertion: rows > 0; each row has Title + Installed/Not-Installed state + Install/Uninstall button

Goal LG2 — skip_update=true optimizes loading

  • Action (UI): open dialog with flag set (URL param or setting)
  • Observable: loading time; network log
  • Assertion: no git fetch calls; load time < N seconds

6.3 GET /customnode/alternatives

Goal LA1 — Show alternatives on dedicated UI flow

  • Action (UI): trigger alternatives display (specific button or context)
  • Observable: alternatives panel/dialog
  • Assertion: populated with alter-list.json entries (at least one if data exists)

6.4 GET /v2/externalmodel/getlist

Goal LM1 — Populate Model Manager dialog

  • Action (UI): open Model Manager
  • Observable: UI grid
  • Assertion: rows > 0; each row has installed flag accurately reflecting filesystem

Goal LM2 — Install flag correctness

  • Precondition: pre-existing model file at known path
  • Action (UI): open Model Manager
  • Assertion: that model row shows "Installed"

6.5 GET /v2/customnode/versions/{node_name}

Goal LV1 — Version dropdown on Install click

  • Action (UI): click Install on a CNR pack row
  • Observable: version selector (select multiple) in modal
  • Assertion: dropdown options match CNR registry versions for that pack; "latest" option present

Goal LV2 — Unknown pack → 400 (UI should handle)

  • Action: direct API call with bogus node_name
  • Assertion: 400; UI shows error or skips

6.6 GET /v2/customnode/disabled_versions/{node_name}

Goal LDV1 — Show disabled versions for re-enable

  • Precondition: pack has disabled versions
  • Action (UI): open pack's "Disabled Versions" dropdown
  • Observable: dropdown options
  • Assertion: options match backend disabled_versions response

6.7 POST /v2/customnode/install/git_url

Goal LGU1 — Install via "Install via Git URL" button

  • Precondition: security_level ≥ high+; pack absent
  • Action (UI): click button → enter URL → confirm
  • Observable: custom_nodes// + .git/
  • Assertion: pack cloned; UI reflects

Goal LGU2 — Security gate

  • Precondition: security_level < high+
  • Assertion: 403 from API; UI shows error; no clone occurs

6.8 POST /v2/customnode/install/pip

Goal LPP1 — Install pip packages via UI

  • Action (UI): trigger pip install flow with known package
  • Observable: venv site-packages
  • Assertion: package importable after install

Goal LPP2 — Security gate

  • Assertion: 403 at lower security; no pip invocation

6.9 GET /v2/manager/notice

Goal LN1 — Fetch and display News

  • Precondition: GitHub reachable
  • Action: GET manager/notice (triggered on Manager menu open)
  • Assertion: HTML response; contains expected markdown-body content; footer has ComfyUI + Manager version

Goal LN2 — Graceful on GitHub unreachable

  • Precondition: network blocked
  • Assertion: "Unable to retrieve Notice" returned; no server crash; UI shows message

Goal LN3 — Non-git ComfyUI warning

  • Precondition: ComfyUI dir not a git repo
  • Assertion: response starts with "isn't git repo" warning paragraph

Goal LN4 — Outdated ComfyUI warning

  • Precondition: comfy_ui_commit_datetime.date() < required_commit_datetime.date()
  • Assertion: response starts with "too OUTDATED!!!" paragraph

Section 7 — Cross-cutting Intent Pattern Templates

Reusable verification templates organized by intent category. Apply the template to any scenario of that type.

7.1 User Capability Template

Goal: User accomplishes task X.

Verification structure:

  1. Precondition check: state is F0 (function not yet performed)
  2. Invoke action: endpoint call or UI interaction
  3. Wait for completion: poll status endpoint OR WebSocket event OR filesystem observer
  4. Observable assertions (in order):
    • Response status 2xx
    • Response body schema correct
    • Side-effect state is F1 (function performed)
    • Secondary state changes (counters, history, lists) reflect the change
  5. Negative checks:
    • No orphan state (partial writes, stale locks)
    • Other unrelated state unchanged (blast radius contained)
  6. Cleanup: restore F0 where possible (for idempotent tests)

Examples in this report: A1, A2, U1, UP1, D1, E1, IM1, UA1, R1, S1, SS1, SR1, C2, C5, V3, CV3, LB1-LB5, LGU1, LPP1

7.2 Input Resilience Template

Goal: Bad input must be rejected without side effects.

Verification structure:

  1. Precondition: system in known-good state S0
  2. Action: send malformed/invalid input (craft variants per endpoint):
    • Malformed JSON (raw text, wrong Content-Type)
    • Missing required field
    • Wrong type for field
    • Out-of-range value
    • Extraneous unexpected field (should be ignored)
  3. Observable assertions:
    • Response 4xx (400 typical; 500 only if server constraint like KeyError)
    • Error message identifies the problem (if surfaced)
  4. Negative checks (the critical part):
    • State S1 == S0 (no mutation)
    • Queue/history not populated
    • Filesystem unchanged
    • No downstream side effects (no email, no download, no process start)

Examples: A4, A5, U1 (missing target), IM2, UA3, UC2 edges, QH2, IF2, IF3, IFB2, C3, LV2

7.3 Security Boundary Template

Goal: Operations requiring privilege X must fail at lower privilege.

Verification structure:

  1. Precondition: server running at specific security_level below the gate
  2. Action: invoke the privileged endpoint
  3. Observable assertions:
    • Response 403
    • Server log contains security-denied message
  4. Critical negative checks:
    • NO task queued
    • NO filesystem change
    • NO network operation initiated
    • NO config change
    • NO process change (no restart, no exit)
  5. Positive counterpart: at or above required level, operation succeeds

Security tiers to cover:

  • middle — reboot, snapshot/remove, _uninstall, _update
  • middle+ — update_all, snapshot/restore, _install_custom_node, _install_model
  • high+ — comfyui_switch_version, install/git_url, install/pip, non-safetensors model, _fix (middlehigh in commit c8992e5d; subsequent highhigh+ in WI-#235 to align the gate with the SECURITY_MESSAGE_HIGH_P log text)

Path-traversal sub-template (within security boundary):

  1. Action: request with ../, absolute path, encoded traversal
  2. Assertion: 400 "Invalid target" or similar
  3. Negative check: target file unchanged; no files outside the endpoint's scope accessed

Examples: IM4, UA2, SR3, SR4, SR6, V5, CV4, LGU2, LPP2, QH2

7.4 Observability Template

Goal: Caller can trust the response to reflect actual system state.

Verification structure:

  1. Establish known state: inject a known delta (queue a task, install a pack, save a snapshot)
  2. Read endpoint: GET the observability endpoint
  3. Assertion: response reflects the known delta
  4. Consistency check: read again immediately; result identical (no race/jitter)
  5. Schema check: all expected fields present with correct types

Required-identity fields (traceability sub-pattern):

  • client_id + ui_id must be present in inputs for all state-mutating endpoints
  • Verify that history/status queries can locate tasks by these IDs

Examples: QS1, QS2, QH1, QH3, IL1, IL2, SL1, C1, V1, V2, CV1, LG1, LM1, LM2, LV1, LDV1

7.5 Idempotency Template

Goal: Re-invoking an operation on an already-satisfied state is safe.

Verification structure:

  1. Invoke operation to reach state F1
  2. Invoke same operation again (no intermediate state change)
  3. Assertions:
    • Response 2xx (same or semantically equivalent status, e.g., 200/201)
    • State remains F1 (no regression, no duplicate)
    • No error raised
  4. Stress variant: invoke N times in rapid succession; ensure no race condition

Examples: U2, UP2, D2, E2, R2, S2, SS2 (distinct but both succeed), SR2, C6

7.6 Data Integrity Template

Goal: Config / runtime constants remain stable and consistent.

Verification structure:

  1. Read constant twice (consecutive or across operations)
  2. Assertion: identical values
  3. Persistence across restart: set value → reboot → re-read
  4. Assertion: value persisted

Examples: V1 (version idempotent), C2 (db_mode survives restart), CV1 (versions list stable)

7.7 Recovery Template

Goal: System can recover from failure or drifted state.

Verification structure:

  1. Induce failure state: delete dependency, corrupt a file, disable a pack
  2. Invoke recovery operation: fix, restore, re-enable
  3. Assertion: state is healthy again; pack/module loads
  4. Negative check: no data loss; original files preserved where applicable

Examples: F1 (fix dependency), E1 (enable disabled), SR5 (restore snapshot)

7.8 Concurrency Safety Template

Goal: Parallel or duplicated operations don't corrupt state.

Verification structure:

  1. Setup parallel trigger: fire multiple start/reset/task commands in rapid succession
  2. Assertions:
    • No duplicate worker thread (single pid in logs)
    • No task processed twice (idempotent task-id check)
    • No race-condition data corruption (e.g., half-written config)
  3. Stress test variant: N parallel requests, assert linearized outcome

Examples: S2 (duplicate start), R1 (reset during processing)


Section 8 — Verification Matrix Summary

Intent category Template Scenarios using Test type needed
User capability 7.1 62 E2E effect verification (real install/remove/save etc.)
Input resilience 7.2 32 Negative tests with negative-check assertions
Security boundary 7.3 15 Permission tests at each security level
Observability 7.4 16 GET correctness + traceability tests
Idempotency 7.5 14 Repeat-call tests
Data integrity 7.6 8 Cross-restart persistence tests
Recovery 7.7 5 Fault-injection tests
Concurrency safety 7.8 2 Parallel-call stress tests

Section 9 — Practical Implementation Priority

Based on security impact + existing coverage gaps:

🔴 Must-add (security + integrity)

  1. Path traversal tests for snapshot/remove, snapshot/restore, queue/history (Section 7.3 path sub-template)
  2. Security gate 403 tests for each tier — requires running with restricted security_level in separate test run
  3. Config persistence across restart — set db_mode → reboot → verify (Section 7.6)

🟡 Should-add (coverage quality)

  1. UI-driven install/uninstall flow (LB1-LB3) — convert debug-install-flow.spec.ts to assertion test
  2. install_model effect — current test only checks 200; add queue/status verification (Goal IM1)
  3. Fix recovery test — induce broken dependency, verify fix heals (Goal F1)

🟢 Nice-to-have

  1. Concurrency tests (duplicate queue/start; parallel task queueing)
  2. get_current snapshot content fidelity — compare to actual installed state
  3. Update version bump verification — test install v1.0.0 → update → expect v1.0.1 marker

Section 10 — Security Mitigation Contracts

Layer: CSRF method-rejection (GET→POST conversion, commit 99caef55). This section formalizes the contract exercised by TWO test files — one per server variant (mutex loading via --enable-manager-legacy-ui):

  • tests/e2e/test_e2e_csrf.py — glob server (4 functions / 26 parametrized invocations — post-WI-HH; was 29 before the 3 dual-purpose endpoints were removed from the reject-GET fixture)
  • tests/e2e/test_e2e_csrf_legacy.py — legacy server (4 functions / 26 parametrized invocations; WI-FF added, WI-GG audit-integrated)

Scope is deliberately narrow — see each test file's SCOPE docstring: only the method-reject layer, not Origin/Referer/same-site/anti-token defenses (those are handled by origin_only_middleware at the aiohttp layer and are out of scope here). The full 16-endpoint inventory is recorded in reports/endpoint_scenarios.md under "CSRF Method-Reject Contract Inventory"; the legacy file substitutes /v2/manager/queue/batch for /v2/manager/queue/task (glob-only) and scopes the 3 dual-purpose endpoints (db_mode, policy/update, channel_url_list) to the ALLOW-GET class only.

10.1 CSRF Method-Reject Contract

Goal CSRF-M1 — State-changing endpoints reject HTTP GET (13 endpoints, post-WI-HH)

  • Precondition: ComfyUI running; the 13 state-changing endpoints from STATE_CHANGING_POST_ENDPOINTS declared as @routes.post(...) in comfyui_manager/glob/manager_server.py (mirror in comfyui_manager/legacy/manager_server.py) — post-WI-HH count; excludes the 3 dual-purpose endpoints (db_mode, policy/update, channel_url_list) whose GET path legitimately reads the current value
  • Action: HTTP GET to each endpoint path (no body, allow_redirects=False, module-scoped ComfyUI fixture)
  • Observable: HTTP status code; response body (first 200 chars on failure)
  • Assertion: status_code not in range(200, 400) AND status_code in (400, 403, 404, 405) for every path in the 13-endpoint fixture (post-WI-HH; the legacy counterpart also iterates 13 paths after substituting queue/batch for queue/task)
  • Negative check: no 2xx success and no 3xx redirect on any state-changing path (blocks <img src=...>, link-click, and redirect-based cross-origin triggers — CVSS 8.1 vector from XlabAI/Tencent Xuanwu report)
  • Test reference (glob): tests/e2e/test_e2e_csrf.py::TestStateChangingEndpointsRejectGet::test_get_is_rejected (1 function × 13 parametrized invocations — post-WI-HH)
  • Test reference (legacy): tests/e2e/test_e2e_csrf_legacy.py::TestLegacyStateChangingEndpointsRejectGet::test_get_is_rejected (1 function × 13 parametrized invocations — queue/batch replaces queue/task; the 3 dual-purpose endpoints are covered via the read-path under CSRF-M3)

Goal CSRF-M2 — POST counterparts still succeed (positive control)

  • Precondition: ComfyUI running; clean snapshot state
  • Action: POST /v2/manager/queue/reset (no-op reset), then POST /v2/snapshot/save (creates a snapshot, cleaned up via /v2/snapshot/remove)
  • Observable: HTTP 200 response on both POSTs; on save, the new entry is observable via /v2/snapshot/getlist
  • Assertion: status_code == 200 for both POSTs; cleanup removes the just-created snapshot
  • Negative check: the CSRF fix must NOT break the legitimate POST path (regression guard — functional equivalence of the converted endpoints must hold)
  • Test reference (glob): tests/e2e/test_e2e_csrf.py::TestCsrfPostWorks (2 functions: test_queue_reset_post_works, test_snapshot_save_post_works)
  • Test reference (legacy): tests/e2e/test_e2e_csrf_legacy.py::TestLegacyCsrfPostWorks (2 functions — same endpoints, legacy server fixture)

Goal CSRF-M3 — Read-only endpoints still allow GET (negative control, 11 endpoints)

  • Precondition: ComfyUI running
  • Action: HTTP GET to 11 read-only endpoints: /v2/manager/version, /v2/manager/db_mode, /v2/manager/policy/update, /v2/manager/channel_url_list, /v2/manager/queue/status, /v2/manager/queue/history_list, /v2/manager/is_legacy_manager_ui, /v2/customnode/installed, /v2/snapshot/getlist, /v2/snapshot/get_current, /v2/comfyui_manager/comfyui_versions
  • Observable: HTTP status code
  • Assertion: status_code == 200 for every read-only path
  • Negative check: the CSRF fix must NOT over-correct by making pure-read endpoints POST-only (would break UI flows that <img>-safe-read via GET). Note: the 3 dual-purpose endpoints (db_mode, policy/update, channel_url_list) appear in BOTH CSRF-M1 (write path requires POST; plain GET is rejected) AND CSRF-M3 (read path still answers GET 200); this dual appearance is intentional — they were split into GET (read) + POST (write) by 99caef55.
  • Test reference (glob): tests/e2e/test_e2e_csrf.py::TestCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds (1 function × 11 parametrized invocations)
  • Test reference (legacy): tests/e2e/test_e2e_csrf_legacy.py::TestLegacyCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds (1 function × 11 parametrized invocations — same endpoint list)

10.2 Out-of-Scope CSRF Layers (tracked for future verification)

  • Origin/Referer validationorigin_only_middleware (aiohttp layer); not exercised by test_e2e_csrf.py
  • Same-site cookie enforcement — browser-layer concern; not server-testable in isolation
  • Anti-CSRF token verification — not implemented in current codebase
  • Cross-site form POST defense — subsumed by Origin validation above

These remain Goals for future work; do not infer coverage from Goals CSRF-M1/M2/M3.


End of Verification Design Report