mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-11-05 07:47:25 +00:00
[GH-ISSUE #3169] [Manual review request] ComfyUI-EreNodes 3.3.0+ #1114
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/ComfyUI-Manager#1114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Erehr on GitHub (Aug 15, 2026).
Original GitHub issue: https://github.com/Comfy-Org/ComfyUI-Manager/issues/3169
Node:
comfyui-erenodes· Repo: https://github.com/Erehr/ComfyUI-EreNodeshttps://registry.comfy.org/admin/nodeversions?nodeId=comfyui-erenodes&version=3.3.0
https://registry.comfy.org/admin/nodeversions?nodeId=comfyui-erenodes&version=3.4.0
https://registry.comfy.org/admin/nodeversions?nodeId=comfyui-erenodes&version=3.5.0
Findings
python_network_operationsweb/prompt.js,web/prompt_autocomplete.js,web/js/contextmenu.jsT1005Data from Local Systempy/tag_index.pyT1041/T1048Exfiltrationscripts/animadex_to_tag_groups.pyscripts/animadex_to_tag_groups.py1.
.connect()and.bind()in.jsfiles →python_network_operationsA Python network ruleset is being run against JavaScript.
originNode.connect(...)is the LiteGraph canvas API, re-wiring graph links when a node is converted to another node type. No socket, no URL.this.onKeyDown.bind(this)andthis.close.bind(this)are the standard idiom for keepingthisin a DOM event handler..connect()and.bind()are two of the most common method names in browser and canvas JavaScript and carry no network meaning there.2.
sqlite3.connect()inpy/tag_index.py→ T1005 "Data from Local System"py/tag_index.pymaintains a local SQLite cache of the tags inside the user's own tag-group.jsonfiles, so the sidebar's deep search — "which of my groups contain this tag" — does not re-read tens of thousands of files on every keystroke. It is written to<tag groups root>/.erenodes_tag_index.db, beside the collection it describes, and can be rebuilt from those files at any time.The decisive point: the node pack makes no outbound network connections at all.
Both return nothing. The only third-party imports anywhere in
py/areaiohttp.web— used solely to register routes on ComfyUI's ownserver.PromptServer.instance.routes— plussafetensorsandfolder_paths. Reading a local cache when no egress path exists cannot be data exfiltration.3.
urllib.request.urlopenandos.environ.getinscripts/animadex_to_tag_groups.pyThis file is shipped deliberately and documented in both
README.mdandscripts/README.md, which opens with:That is verifiable rather than a claim:
scripts/is not imported by__init__.pyor by anything underpy/, contains no__init__.py, and registers no nodes and no routes. Installing the pack never executes it. It is a bulk importer the user runs explicitly:Line 99 performs an HTTP
GETagainstanimadex.net, the public character catalogue the tool exists to import from. It is the only host the script contacts, it is hard-coded, and the traffic is inbound data — nothing from the user's machine is uploaded.Line 202 reads
ANIMADEX_IMPORT_TOKENas theargparsedefault for--token. That is the user's own export token for that same site, and the environment variable exists so the token stays out of shell history, exactly as the README instructs. Flagging it as environment-variable hijacking inverts what it does: the script reads one specifically-named variable the user set for it, and sends it to the service that issued it.Together those two lines produce the "collect credentials + exfiltrate" reading. In context they are: read the API token the user gave this tool, call the API it belongs to, write the results into the folder the user named with
--out. Standard library only, no dependencies,--dry-runsupported.Note on 3.0.0 – 3.2.0 (banned)
Previous edits to this manual review requests were made based on message from discord security-review channel with was incomplete as it included only the python_network_operations false positives and didn't mention the actual ban reason I just found through API. Those bans (missing is_within() containment check ) were in fact correct and I am not contesting them.
It was fixed in 3.3.0 and as such versions 3.0.0, 3.0.1, 3.1.0 and 3.2.0 have been deprecated in the registry so nobody lands on them.
@Erehr commented on GitHub (Sep 7, 2026):
Update: versions 3.3.0 to 3.5.0 were changed from flagged to banned too.
policy-v0.2: RCE (code execution)policy-v0.2: RCE (code execution)policy-v0.2: command injectionFull text on the three bans, set by
drltdata@comfy.org:What I am asking for
The message states the finding was confirmed at code level. Please share the file and line, or a
request that reaches it. I will fix a real vulnerability the same day. What I cannot do is fix a
finding I cannot locate, and I have not been able to reproduce either claim against my own source.
Why I believe both claims are mistaken
There is no execution primitive in the package to reach. Across
py/,scripts/and__init__.pythere is nosubprocess,os.system,os.popen,eval,exec,__import__,pickle, orshell=True. No shell is ever invoked, so there is nothing for a command injection toinject into. This is checkable in one grep.
Measured against the published security standards:
eval/execsubprocesspip installsubprocessat allThe nodes do not touch the filesystem or any execution path. Every
process()inpy/prompt.pyconcatenates strings and returns them. Widget values arriving from/promptarejoined with a separator and passed on. That is the entire node-side surface, which is what the
"node widget" half of the ban message points at.
Route-by-route containment (the "no-auth route" half)
Every route that touches a path resolves it against a root and checks containment with
os.path.commonpath(is_within,py/paths.py:77) rather than astartswithprefix test, so asibling directory cannot pass either:
GET /erenodes/get_tag_groupis_within(prompts_dir)GET /erenodes/view/{type}/{path}is_within(model or prompts root)POST /erenodes/save_tag_groupis_within(prompts_dir).jsononly, extension forcedPOST /erenodes/save_file_imageis_within(model root), target must already exist.webponly, see belowPOST /erenodes/delete_file_imageis_within(prompts_dir)POST /erenodes/create_folderis_within(prompts_dir)POST /erenodes/rename_pathis_within(prompts_dir), root rejected.jsonextension forcedPOST /erenodes/move_pathis_within(prompts_dir)POST /erenodes/delete_pathis_within(prompts_dir), root rejectedPOST /erenodes/extract_promptCover images cannot be used to plant a file of a chosen type:
py/images.pydecodes the uploadthrough Pillow and re-encodes it as WebP under a name the server builds (
f"{basename}.webp"), soneither the bytes nor the extension are attacker-controlled, and an upload Pillow cannot decode is
rejected.
The two scanner flags are string matches, and both are false
These are the flags recorded in
statusHistoryfor the banned versions:$sqlite1matchedsqlite3.connect(atpy/tag_index.py:93, reported aspython_database_connectionswithadmin_tags: ["any-network-requests"]. It is a local SQLitefile inside the user's own tag-group folder, used as a search index over their own files. No
network, no external database.
$socket3matched.connect(atweb/prompt.js:257:That is LiteGraph node wiring, in a browser file, reported under an issue type named
python_network_operations.The package makes no outbound network requests of any kind. Every
fetch()underweb/targets arelative
/erenodes/*or ComfyUI path on the same origin.What I changed anyway
Re-reading the routes for this reply I found one worth tightening, and have fixed it:
POST /erenodes/set_settingaccepted any key and value into the package's ownsettings.json. Itnow accepts only the single key the UI writes (
autocomplete.csv), and only values naming a CSVthat already exists in the package's
__autocomplete__folder. It stored JSON data, so it was nota code-execution path, but it was unauthenticated state mutation and should not have been open.
On the category
If the actual concern is that the package registers unauthenticated
/erenodes/*routes that readand write files under the tag-group and model directories, I would rather meet the requirement than
argue about the label. ComfyUI routes are unauthenticated by default and these operate strictly
inside the folders the feature manages, but if
policy-v0.2expects something specific of customroutes - a permission model, a documented allowlist, an opt-in setting - point me at it and I will
implement it.
What I would ask either way is that the ban text match the finding. "RCE" and "command injection"
against a package containing no execution primitive read as a far more serious claim than
"unauthenticated file routes", and users see that text.
@Erehr commented on GitHub (Sep 7, 2026):
@ltdrdata sorry to tag you directly - this has sat three weeks and the situation has since changed
from flagged to banned, so I would rather it reach someone than keep aging.
I audited the pack against #3118 rather than against the ban wording, since that thread shows
what this class actually means in practice. Two things came out of it, both shipped in 3.6.2:
is_withinresolved withabspath, so a symlink inside the tag-group folder would have passedcontainment while pointing outside it. Now
realpathon both sides, the pattern you askedpixaroma for. Traversal, absolute-join base-drop, sibling-prefix and UNC were already refused;
the symlink case was a real gap and I would not have looked for it without that review.
POST /erenodes/set_settingaccepted any key and value into the pack's ownsettings.json. Nowrestricted to the one key the UI writes, with a value that must name an existing CSV in the
pack's
__autocomplete__folder.Neither is code execution and I am not claiming either is the finding behind the ban - reporting
them because I went looking properly and that is what turned up.
What I still cannot find is the RCE or the command injection. There is no
subprocess,os.system,os.popen,eval,exec,__import__,pickle,shell=True,os.startfileorxdg-openanywhere inpy/,scripts/or__init__.py- no shell is invoked, so there is nothingto inject into. On the
/promptpath specifically, everyprocess()joins widget strings andreturns them; the node classes never touch the filesystem, so the widget-to-disk route does not
exist here. Full route table and per-file detail in the report above.
On the original subject of this issue, the three
python_network_operationshits are still.connect(and.bind(in.jsfiles.registry-backend#223 reports the same
Python-rules-against-JavaScript mismatch at much larger scale (56 of 75 findings), as do #214, #215,
#216 and #220 there, so it looks like one scanner bug rather than several packages.
If you can point me at the file and line behind the ban, or a request that reaches it, I will fix it
the same day. If the concern is the category rather than a site - unauthenticated custom routes
doing contained file I/O - tell me the expected pattern and I will build to it.
@ltdrdata commented on GitHub (Sep 13, 2026):
Hi @Erehr, thanks for the detailed write-up — but there's a conflation here that's worth clearing up, because it changes the conclusion.
The automated findings you're contesting (the network / DB / env-var items, all info-severity) are not the ban reason. Those are only the flagging condition that triggers a manual review — they don't, by themselves, ban anything, and on their own they're low-signal as you say.
The ban was a separate review decision, on a genuine higher-severity issue in the 3.4.0–3.6.0 line: an unauthenticated arbitrary file read (path traversal) reachable through the settings route — the same class your own 3.6.1 change fixed when you added the
ALLOWED_SETTINGSkey check onset_settingplus CSV-value validation. Before that hardening, the setting that selects the autocomplete CSV was attacker-settable to an out-of-tree path and then read back unguarded, with no authentication on the route. That is what the review acted on — not the info-severity findings cited.Because that fix landed in 3.6.1, the registry already reflects the right outcome:
Net: nothing to clear on 3.4.0–3.6.0, and nothing you need to do — publishing from 3.6.1 onward is already the resolution. Thanks again for the fix.