The notice route fetched the News page from GitHub with
verify_ssl=False, so certificate verification was off for every user:
whoever sat on the connection could present any certificate for
github.com and choose the notice text and links. The sanitizer limits
the markup, but it cannot tell whose content it is.
- Turn verification off for the notice only when bypass_ssl is set, as
manager_util.get_data does
- Answer a certificate or connection failure with "Unable to retrieve
Notice" where it raised and the notice area showed the server's 500
page
- Add tests that run get_notice against a local HTTPS server behind a
local CONNECT proxy: an untrusted certificate and a trusted
certificate for another host are rejected; bypass_ssl accepts the
untrusted certificate; a CA trusted through SSL_CERT_FILE is
accepted; a closed proxy, a proxy answering 502 and a proxy that
never answers end in the unavailable notice, each with one warning
naming the cause
- Add a CHANGELOG entry
Fixes#3302
Four of the six share destinations are gone: the comfyworkflows.com
domain registration has expired, the Copus API answers 410 "ComfyUI
direct publishing is no longer supported", the OpenArt workflow API and
pages return 404 or redirect to the home page, and esheep.com no longer
resolves. Manager still offered them, and the ComfyWorkflows flow would
hand the share key, the workflow and the installed snapshot to whoever
answers at that domain next.
- Remove ComfyWorkflows, Copus, OpenArt and eSheep from the share
chooser, the Share setting, the Workflow Gallery menu and the share
dialog, with their server routes, the two dialog modules and what the
Copus theme added to the chooser
- Treat a saved Share setting that names a removed site as All, and do
not open a removed site saved as the last visited Workflow Gallery
- Sharing to YouML and to Matrix is unchanged; the share request no
longer carries cw_auth or is_nsfw, which only ComfyWorkflows used
- Update README and CHANGELOG, drop the tests of the removed code, and
keep a single-quoted href under test through the update-result links
os.execv on Windows joins its arguments with spaces and quotes nothing, so
only the interpreter and script path (quoted by hand) survived a restart.
Any other argument with a space, e.g. --browser-profile "Profile 1", was
split in two and ComfyUI failed to start with 'unrecognized arguments'.
Build the command in one helper, manager_util.build_restart_command, used by
both the reboot endpoint and the prestartup restart. On Windows every entry
is quoted with subprocess.list2cmdline; other platforms are unchanged.
Fixes#3339
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.
Two defects, the second hidden behind the first. CI runs ruff only, so
neither shows up there.
The generic-403 guard counted the copy across all of js/common.js while its
docstring describes exactly the two occurrences inside handle403Response. A
third, unrelated use at line 722 builds the batch-uninstall error message, so
the file-wide count tripped on it. Scoped the count to the handle403Response
block via the existing _js_function_block helper. This part is not
platform-specific; the count is 3 on Linux and Windows alike.
With that passing, the test reached a loop calling js_file.read_text() with no
encoding. That uses the locale encoding, so on cp1252 Windows it raises
UnicodeDecodeError on the four js files containing emoji. Passed
encoding="utf-8" explicitly, and did the same for the other four read_text
calls in the file, which read ASCII sources today and so pass by luck.
* 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)