`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
63 lines
2.0 KiB
Python
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()
|