Skip to content

Commit 9a9fdb1

Browse files
Harden some nodes against potential issues related to combos. (Comfy-Org#15277)
1 parent 16e3f30 commit 9a9fdb1

5 files changed

Lines changed: 39 additions & 11 deletions

File tree

‎AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,12 @@
305305

306306
- Follow existing node conventions: `INPUT_TYPES`, `RETURN_TYPES`, `FUNCTION`,
307307
`CATEGORY`, and registration through the local mapping used by that file.
308+
- Treat legacy combo inputs, `io.Combo`, and `io.DynamicCombo` values as
309+
untrusted when they affect filesystem access. Any value used as a file or
310+
folder name, path component, format, or extension must be validated again at
311+
the load/save boundary using an existing `folder_paths` resolver or
312+
containment helper, or a fixed allowlist/mapping. Do not rely only on the
313+
advertised combo options or prompt validation.
308314
- Keep node changes backward compatible by default. Add inputs with sensible
309315
defaults and avoid changing output types unless the request requires it.
310316
- Model implementations should add the minimal number of ComfyUI nodes required

‎comfy_api/latest/_ui.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def get_save_animated_webp_ui(
260260
class AudioSaveHelper:
261261
"""A helper class with static methods to handle audio saving and metadata."""
262262
_OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
263+
_FORMATS = {"flac", "mp3", "opus"}
263264

264265
@staticmethod
265266
def save_audio(
@@ -270,6 +271,9 @@ def save_audio(
270271
format: str = "flac",
271272
quality: str = "128k",
272273
) -> list[SavedResult]:
274+
if format not in AudioSaveHelper._FORMATS:
275+
raise ValueError(f"Unsupported audio format: {format!r}")
276+
273277
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
274278
filename_prefix, _get_directory_by_folder_type(folder_type)
275279
)

‎comfy_extras/nodes_dataset.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def define_schema(cls):
195195

196196
@classmethod
197197
def execute(cls, folder):
198-
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
198+
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
199199
valid_extensions = [".png", ".jpg", ".jpeg", ".webp"]
200200
image_files = [
201201
f
@@ -241,7 +241,7 @@ def define_schema(cls):
241241
def execute(cls, folder):
242242
logging.info(f"Loading images from folder: {folder}")
243243

244-
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
244+
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
245245
valid_extensions = [".png", ".jpg", ".jpeg", ".webp"]
246246

247247
image_files = []
@@ -310,7 +310,7 @@ def define_schema(cls):
310310

311311
@classmethod
312312
def execute(cls, folder):
313-
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
313+
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
314314
video_files = sorted([
315315
f for f in os.listdir(sub_input_dir)
316316
if any(f.lower().endswith(ext) for ext in VALID_VIDEO_EXTENSIONS)
@@ -357,7 +357,7 @@ def define_schema(cls):
357357

358358
@classmethod
359359
def execute(cls, folder):
360-
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
360+
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
361361

362362
video_files = []
363363
for item in sorted(os.listdir(sub_input_dir)):

‎comfy_extras/nodes_gaussian_splat.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ def _mat_to_quat(m):
471471

472472

473473
class SplatToFile3D(IO.ComfyNode):
474+
FORMAT_WRITERS = {
475+
"ply": _gaussian_ply_bytes,
476+
"ksplat": _gaussian_ksplat_bytes,
477+
"spz": _gaussian_spz_bytes,
478+
}
479+
474480
@classmethod
475481
def define_schema(cls):
476482
return IO.Schema(
@@ -482,7 +488,7 @@ def define_schema(cls):
482488
"Supports one item per batch only.",
483489
inputs=[
484490
IO.Splat.Input("splat"),
485-
IO.Combo.Input("format", options=["ply", "ksplat", "spz"], # TODO: add "splat" when we have a writer for it
491+
IO.Combo.Input("format", options=list(cls.FORMAT_WRITERS), # TODO: add "splat" when we have a writer for it
486492
tooltip="ply: standard 3D Gaussian Splat with full spherical harmonics. "
487493
"ksplat: mkkellogg SplatBuffer (level 0, uncompressed), base color only "
488494
"spz: Niantic gzip-compressed (~10x smaller), base color only "
@@ -493,10 +499,13 @@ def define_schema(cls):
493499

494500
@classmethod
495501
def execute(cls, splat, format="ply") -> IO.NodeOutput:
502+
writer = cls.FORMAT_WRITERS.get(format)
503+
if writer is None:
504+
raise ValueError(f"Unsupported splat format: {format!r}")
505+
496506
if splat.positions.shape[0] > 1:
497507
logging.warning("SplatToFile3D supports one item per batch only. Got %d; using first.", splat.positions.shape[0])
498508
end = _real_len(splat, 0)
499-
writer = {"ksplat": _gaussian_ksplat_bytes, "spz": _gaussian_spz_bytes}.get(format, _gaussian_ply_bytes)
500509
data = writer(splat.positions[0, :end], splat.scales[0, :end],
501510
splat.rotations[0, :end], splat.opacities[0, :end], splat.sh[0, :end])
502511
return IO.NodeOutput(Types.File3D(BytesIO(data), file_format=format))

‎nodes.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -633,30 +633,39 @@ class DiffusersLoader:
633633
SEARCH_ALIASES = ["load diffusers model"]
634634

635635
@classmethod
636-
def INPUT_TYPES(cls):
636+
def _model_paths(cls):
637637
paths = []
638638
for search_path in folder_paths.get_folder_paths("diffusers"):
639639
if os.path.exists(search_path):
640640
for root, subdir, files in os.walk(search_path, followlinks=True):
641641
if "model_index.json" in files:
642642
paths.append(os.path.relpath(root, start=search_path))
643+
return paths
643644

644-
return {"required": {"model_path": (paths,), }}
645+
@classmethod
646+
def INPUT_TYPES(cls):
647+
return {"required": {"model_path": (cls._model_paths(),), }}
645648
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
646649
FUNCTION = "load_checkpoint"
647650
DEPRECATED = True
648651

649652
CATEGORY = "model/loaders"
650653

651654
def load_checkpoint(self, model_path, output_vae=True, output_clip=True):
655+
if model_path not in self._model_paths():
656+
raise ValueError(f"Invalid diffusers model path: {model_path!r}")
657+
658+
resolved_model_path = None
652659
for search_path in folder_paths.get_folder_paths("diffusers"):
653660
if os.path.exists(search_path):
654661
path = os.path.join(search_path, model_path)
655-
if os.path.exists(path):
656-
model_path = path
662+
if os.path.isfile(os.path.join(path, "model_index.json")):
663+
resolved_model_path = path
657664
break
665+
if resolved_model_path is None:
666+
raise FileNotFoundError(f"Diffusers model {model_path!r} not found.")
658667

659-
return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
668+
return comfy.diffusers_load.load_diffusers(resolved_model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
660669

661670

662671
class unCLIPCheckpointLoader:

0 commit comments

Comments
 (0)