mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-03 21:20:49 +08:00
- test #3: guard the symlink-escape test with a try/except skip so it no longer errors on Windows CI where os.symlink needs elevated privileges / Developer Mode (mirrors the guard in the sibling test #2). - test #5: refresh the stale module docstring to describe the actual /view gating (view_image closure calling folder_paths.is_dangerous_content_type, the normalising check) instead of the bypassable raw set-membership test.
166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
"""Security tests for GHSA-779p-m5rp-r4h4 — FIX #3.
|
|
|
|
Path traversal in folder_paths.get_annotated_filepath / exists_annotated_filepath,
|
|
plus the shared is_within_directory() containment helper.
|
|
|
|
These are pure-function tests (no running server). The input/output/temp
|
|
directories are pointed at tmp_path via the folder_paths setters, so a crafted
|
|
name containing `../`, an absolute path, or a symlink that escapes the base
|
|
directory must be rejected.
|
|
|
|
Reference: https://github.com/Comfy-Org/ComfyUI/security/advisories/GHSA-779p-m5rp-r4h4
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import folder_paths
|
|
from comfy.options import enable_args_parsing
|
|
enable_args_parsing()
|
|
|
|
|
|
@pytest.fixture
|
|
def sandbox(tmp_path):
|
|
"""Point folder_paths' input/output/temp dirs at a real temp sandbox.
|
|
|
|
Yields the realpath'd base, input, output and temp directories. The original
|
|
directory values are restored afterward so tests stay isolated.
|
|
"""
|
|
base = os.path.realpath(str(tmp_path))
|
|
input_dir = os.path.join(base, "input")
|
|
output_dir = os.path.join(base, "output")
|
|
temp_dir = os.path.join(base, "temp")
|
|
for d in (input_dir, output_dir, temp_dir):
|
|
os.makedirs(d, exist_ok=True)
|
|
|
|
orig_input = folder_paths.get_input_directory()
|
|
orig_output = folder_paths.get_output_directory()
|
|
orig_temp = folder_paths.get_temp_directory()
|
|
|
|
folder_paths.set_input_directory(input_dir)
|
|
folder_paths.set_output_directory(output_dir)
|
|
folder_paths.set_temp_directory(temp_dir)
|
|
|
|
yield {
|
|
"base": base,
|
|
"input": input_dir,
|
|
"output": output_dir,
|
|
"temp": temp_dir,
|
|
}
|
|
|
|
folder_paths.set_input_directory(orig_input)
|
|
folder_paths.set_output_directory(orig_output)
|
|
folder_paths.set_temp_directory(orig_temp)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_within_directory() — the shared containment helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_is_within_directory_legit_child(sandbox):
|
|
base = sandbox["input"]
|
|
child = os.path.join(base, "sub", "image.png")
|
|
assert folder_paths.is_within_directory(base, child) is True
|
|
|
|
|
|
def test_is_within_directory_dotdot_escape(sandbox):
|
|
base = sandbox["input"]
|
|
escape = os.path.join(base, "..", "..", "etc", "passwd")
|
|
assert folder_paths.is_within_directory(base, escape) is False
|
|
|
|
|
|
def test_is_within_directory_symlink_escape(sandbox):
|
|
"""A symlink created INSIDE base that points OUTSIDE base must not pass.
|
|
|
|
This is the key new hardening: is_within_directory realpath()s both operands,
|
|
so a symlink planted in the base directory can't be used to read files
|
|
elsewhere. We create a real on-disk symlink and a real secret target to
|
|
verify the check actually resolves the link.
|
|
"""
|
|
base = sandbox["input"]
|
|
|
|
# A directory living outside the base, holding a secret file.
|
|
outside = os.path.join(sandbox["base"], "outside_secret_dir")
|
|
os.makedirs(outside, exist_ok=True)
|
|
secret = os.path.join(outside, "secret.txt")
|
|
with open(secret, "w") as f:
|
|
f.write("top secret")
|
|
|
|
# Plant a symlink inside base that points at the outside directory.
|
|
# symlink creation can require elevated privileges / Developer Mode on
|
|
# Windows, so skip cleanly where it isn't available (same guard as the
|
|
# sibling test in test_ghsa_779p_02_preview_traversal.py).
|
|
link = os.path.join(base, "escape_link")
|
|
try:
|
|
os.symlink(outside, link)
|
|
except (OSError, NotImplementedError):
|
|
pytest.skip("symlinks not supported on this platform/filesystem")
|
|
|
|
# Accessing the secret "through" the in-base symlink must be rejected.
|
|
target_via_link = os.path.join(link, "secret.txt")
|
|
assert folder_paths.is_within_directory(base, target_via_link) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_annotated_filepath()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_get_annotated_filepath_legit_name(sandbox):
|
|
result = folder_paths.get_annotated_filepath("image.png")
|
|
assert result == os.path.join(sandbox["input"], "image.png")
|
|
assert folder_paths.is_within_directory(sandbox["input"], result)
|
|
|
|
|
|
def test_get_annotated_filepath_input_annotation(sandbox):
|
|
result = folder_paths.get_annotated_filepath("image.png [input]")
|
|
assert result == os.path.join(sandbox["input"], "image.png")
|
|
|
|
|
|
def test_get_annotated_filepath_output_annotation(sandbox):
|
|
result = folder_paths.get_annotated_filepath("image.png [output]")
|
|
assert result == os.path.join(sandbox["output"], "image.png")
|
|
|
|
|
|
def test_get_annotated_filepath_temp_annotation(sandbox):
|
|
result = folder_paths.get_annotated_filepath("image.png [temp]")
|
|
assert result == os.path.join(sandbox["temp"], "image.png")
|
|
|
|
|
|
def test_get_annotated_filepath_dotdot_raises(sandbox):
|
|
with pytest.raises(ValueError):
|
|
folder_paths.get_annotated_filepath("../etc/passwd")
|
|
|
|
|
|
def test_get_annotated_filepath_dotdot_with_annotation_raises(sandbox):
|
|
with pytest.raises(ValueError):
|
|
folder_paths.get_annotated_filepath("../../etc/passwd [output]")
|
|
|
|
|
|
def test_get_annotated_filepath_absolute_escape_raises(sandbox):
|
|
with pytest.raises(ValueError):
|
|
folder_paths.get_annotated_filepath("/etc/passwd")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# exists_annotated_filepath()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_exists_annotated_filepath_existing_legit_file(sandbox):
|
|
real = os.path.join(sandbox["input"], "real.png")
|
|
with open(real, "w") as f:
|
|
f.write("data")
|
|
assert folder_paths.exists_annotated_filepath("real.png") is True
|
|
|
|
|
|
def test_exists_annotated_filepath_traversal_returns_false(sandbox):
|
|
"""A traversal name must return False without raising and without probing
|
|
outside the base directory (must never reach os.path.exists for the escape).
|
|
"""
|
|
# /etc/passwd exists on POSIX; the function must still report False because
|
|
# the resolved path escapes the input directory.
|
|
assert folder_paths.exists_annotated_filepath("../../../../../../etc/passwd") is False
|
|
|
|
|
|
def test_exists_annotated_filepath_absolute_returns_false(sandbox):
|
|
assert folder_paths.exists_annotated_filepath("/etc/passwd") is False
|