write_config() rebuilt config.ini's [default] section entirely from the
startup snapshot into a fresh parser opened 'w'. Any Manager settings
change therefore silently reverted values hand-edited while ComfyUI was
running (the same config['default'] flags those messages tell users to
edit), deleted the two read-but-never-written keys (http_channel_enabled,
default_cache_as_channel_url), and erased every non-[default] section.
get_config() now tracks which keys callers actually change, and
write_config() re-reads config.ini and overlays only those keys plus the
live preview_method. After a successful write the just-persisted keys are
cleared from the tracker, so a key changed once through the UI does not
stay dirty for the life of the process and re-clobber a later hand-edit of
that same key on the next unrelated write. Hand-edited values, unknown
keys, and foreign sections survive; CRLF sanitization still applies to
every value written; an unparsable config.ini falls back to a full rewrite
instead of failing every settings endpoint.
configparser.read() suppresses OSError, so a config.ini that exists but
cannot be read comes back as an empty parser rather than an exception,
which would silently route the merge into the bootstrap branch and rewrite
the file from defaults. write_config() now detects that case (the file
exists but read() loaded nothing) and refuses the write with a raised
error, leaving the file untouched, rather than destroying its contents
while reporting success.
tests/test_write_config_persistence.py pins the three loss classes
(hand-edit revert, key deletion, section erasure), the settings
round-trip, the persisted-key re-clobber guard, and the silent-read-failure
refusal.
The install-denial diagnostics named the wrong gate, sending users to
settings that could not resolve their denial:
- SECURITY_MESSAGE_FLAG_GIT_URL / _FLAG_PIP omitted the loopback half of
the flag-AND-loopback predicate, telling users to enable a flag that
was often already enabled. They now state both conditions, print the
live --listen value at every emission site, and explain that both
values are read once at startup (stop the server, edit, then start).
- SECURITY_MESSAGE_MIDDLE_OR_BELOW instructed setting security_level to
'middle', a value that does not exist; it now names only real values
(any level other than 'strong': normal, normal-, weak).
- The blocked-risk arm reused SECURITY_MESSAGE_GENERAL, blaming
security_level although no value of it can help there; it now emits a
dedicated SECURITY_MESSAGE_BLOCKED_RISK stating this is not a
configuration problem.
- SECURITY_MESSAGE_NORMAL_MINUS_MODEL now names its loopback-listener
condition and the same restart guidance.
- The js/common.js dedicated-403 messages mirror the loopback clause.
* feat(security): add dedicated install flags decoupled from security_level
Gate 'install via git URL' and 'install via pip' with dedicated opt-in
boolean flags (allow_git_url_install / allow_pip_install) in config.ini
[default], fully replacing the security_level term on those surfaces
(REPLACE, not AND — a strict level no longer denies when the flag is on;
a weak level no longer allows when the flag is off).
- glob/manager_server.py: pure predicate is_dedicated_install_allowed
(flag AND loopback, request-time args.listen); REPLACE gates at
/customnode/install/git_url and /customnode/install/pip; batch
unknown-URL arm routes through the same full predicate at the risky
position (loopback term is load-bearing — the middle entry gate has
no network-position term; the entry gate itself stays in force);
unknown-pip in batch stays unconditionally blocked; new
SECURITY_MESSAGE_FLAG_* denial constants name the responsible flag;
security_403_response gains flag_token (comfyui_outdated keeps precedence)
- glob/manager_core.py: register both keys (read via get_bool default-false,
write list, exception fallback); "true"-only truthy; restart-only activation
- js/common.js: 403 dialog copy names the responsible flag at the two
install call sites
- README.md: security-policy docs for both flags (per-surface scope incl.
the batch entry-gate qualifier, REPLACE decoupling, loopback bound,
opt-in config snippet, default-deny + migration note); stale tier lists
corrected against the actual gates
- CHANGELOG.md: opt-in migration note + accepted residual risk (flags
bypass the forced-strong outdated-ComfyUI hardening on loopback,
opt-in only), decoupling claim qualified for the batch entry gate
Tests: unit suite (predicate truth table, REPLACE litmus both directions,
AST binding-proofs against live handlers, subprocess-isolated config
contract) plus a real-server E2E suite that mounts the Manager-under-test
via git worktree (exact-SHA pin, detached) against a real ComfyUI and
exercises both flag surfaces and both arms — deny arms (403 + flag-naming
body/log + no install artifact), git-URL allow arm (real clone), pip allow
arm as a two-phase reservation oracle — with zero-residual self-clean.
Module skips without E2E_COMFYUI_ROOT; unit suite unaffected.
The manager-v4 branch ships the identical policy (shared invariants +
config contract); this tree uses the degraded predicate 'flag AND
loopback' (no personal_cloud-equivalent mode here).
* bump version to v3.41
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 5 no-body POST handlers (snapshot/save,
manager/queue/{reset,start,update_comfyui}, manager/reboot) to block
<form method=POST> CSRF that bypasses method-only gating. Convert 10 pure
state-changing endpoints (fetch_updates, queue/{update_all,reset,start,
update_comfyui}, snapshot/{remove,restore,save}, comfyui_switch_version,
reboot) from GET to POST and split 5 config endpoints
(db_mode/preview_method/channel_url_list/policy/{component,update}) into
GET(read) + POST(write, JSON body). Emit the in_progress + done event pair
from the /manager/queue/install sync-enable fast-path so client UI
finalizes (previously only queue/start's empty worker done fired, leaving
item.restart unset and the Enable button visible after a successful enable).
Harden js/custom-nodes-manager.js completion path: await onQueueCompleted
with try/catch (surfaces silent turbogrid stale-item throws), replace the
{}.length == 0 no-op empty guard, set install_context before queue/install
to avoid a sync-completion race, wrap classList/updateCell in try/catch.
Resynchronize openapi.yaml with the converted routes (method → post, query
params → requestBody JSON schema, sibling post on 5 split endpoints).
Update 31 JS fetchApi call sites across 7 files; add
tests/test_csrf_content_type_helper.py covering 5 Content-Type cases via
aiohttp TestClient.
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)
- Add litellm==1.82.7 and litellm==1.82.8 to blacklist (PYSEC-2026-2)
- Add ultralytics==8.3.42 to blacklist
- Replace substring matching with exact version set matching
- Remove early break to detect multiple malicious packages
- Add detection for ComfyUI PR #11261 (per-queue preview override)
- Return DISABLED status when native feature is detected
- Improve UI loading state and prevent flash of enabled state
- Add accessibility attributes and visual feedback for disabled state
- Show user notification when feature transitions to native
- Version bump to 3.39
* Improve comfyui version listing
* Fix ComfyUI semver selection and stable update
* Fix nightly current detection on default branch
* Fix: use tag_ref.name explicitly and cache get_remote_name result
- Use tag_ref.name instead of tag_ref object for checkout
- Cache get_remote_name() result to avoid duplicate calls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Dr.Lt.Data <dr.lt.data@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Use --ff-only flag to detect non-fast-forward situations
- Create backup branch before resetting divergent local branch
- Reset to remote branch when fast-forward is not possible
- Use time.strftime() instead of datetime for better compatibility
- Bump version to 3.38.2
- 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).
* Previously, only `uv` installed inside a venv was properly handled. Now `uv` installed outside the venv is also supported.
* Even if `use_uv=False`, `uv` is used as a fallback when `pip` is unavailable.
* Even if `use_uv=True`, `pip` is used as a fallback when `uv` is unavailable.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2125