Files
Alexopus bc798df250 Fix path traversal in ImageSaver output path/filename handling
`path` and `filename` are free-form strings that can come from a
workflow's JSON, not just a person typing into the local UI. Only the
final path segment was ever sanitized (make_pathname/sanitize_filename),
so a value like `../../../etc` or an absolute path could make
ImageSaver.save_images() create directories and write the image/JSON
sidecar outside ComfyUI's output directory (CWE-22/CWE-73). This is
what caused v1.24.0 to be banned on the Comfy Registry
(Comfy-Org/registry-backend#206).

Add utils.resolve_within_output(), which joins path pieces onto
folder_paths.output_directory, resolves symlinks, and rejects the
result if it escapes that directory. Apply it everywhere
ImageSaver.save_images() turns `path`/`filename` into a filesystem
path: the output directory itself, the final image target, and the
optional workflow-JSON sidecar. Nested relative subfolders (e.g.
`portraits/2026-08-27`) still work; `..`, absolute paths, and
symlink escapes are now rejected before anything is created.

Covered by a new tests/ suite (tests/test_paths.py), since exercising
nodes.py requires stubbing ComfyUI's runtime (see tests/conftest.py).

Fixes alexopus/ComfyUI-Image-Saver#138
2026-08-27 20:52:13 +02:00

63 lines
2.0 KiB
Python

"""
Test harness for exercising ComfyUI-dependent modules (nodes.py, utils.py)
without a full ComfyUI installation.
ComfyUI normally supplies `folder_paths`, `nodes`, and the `comfy.*` package
tree to custom node plugins at load time. Outside of ComfyUI we stand in
minimal stubs for those here.
We load nodes.py/utils.py directly under a synthetic parent package instead
of importing this repository's own __init__.py, because __init__.py eagerly
pulls in nodes (checkpoint/UNET loaders, sampler selectors, etc.) that need
much more of ComfyUI's internals than the plain file-path logic under test.
"""
import importlib.util
import pathlib
import sys
import types
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
PKG_NAME = "image_saver_under_test"
def _stub_module(name: str, **attrs) -> types.ModuleType:
if name in sys.modules:
return sys.modules[name]
module = types.ModuleType(name)
for key, value in attrs.items():
setattr(module, key, value)
sys.modules[name] = module
return module
def _install_comfyui_stubs() -> None:
_stub_module("torch", Tensor=object)
_stub_module("tqdm", tqdm=lambda iterable=None, **kw: iterable if iterable is not None else [])
_stub_module(
"folder_paths",
output_directory=str(REPO_ROOT), # each test overrides this via monkeypatch
supported_pt_extensions={".safetensors", ".ckpt", ".pt"},
get_full_path=lambda *a, **kw: None,
)
_stub_module("nodes", MAX_RESOLUTION=16384)
comfy = _stub_module("comfy")
comfy.__path__ = [] # mark as a package so `comfy.sd1_clip` resolves under it
_stub_module(
"comfy.sd1_clip",
escape_important=lambda *a, **kw: None,
unescape_important=lambda *a, **kw: None,
token_weights=lambda *a, **kw: None,
)
def _register_plugin_package() -> None:
if PKG_NAME in sys.modules:
return
package = types.ModuleType(PKG_NAME)
package.__path__ = [str(REPO_ROOT)]
sys.modules[PKG_NAME] = package
_install_comfyui_stubs()
_register_plugin_package()