Compare commits

...

11 Commits

Author SHA1 Message Date
Varun Chawla
2a65905998
Merge 2657155dfc into b08debceca 2026-07-06 17:34:07 +08:00
Daxiong (Lin)
b08debceca
chore: update embedded docs to v0.5.7 (#14783)
Some checks failed
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Build package / Build Test (3.14) (push) Has been cancelled
2026-07-06 09:56:09 +08:00
comfyanonymous
000c6b784e
Small speedup for text model sampling. (#14773) 2026-07-05 18:39:24 -07:00
Alexander Piskun
985fb9d6ad
[Partner Nodes] fix(logs-auth): mask authorization headers in logs (#14774)
Some checks failed
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Generate Pydantic Stubs from api.comfy.org / generate-models (push) Has been cancelled
Signed-off-by: bigcat88 <bigcat88@icloud.com>
2026-07-05 13:55:29 +03:00
Alexis Rolland
7f287b705e
fix: Bug when setting transparency in color picker (#14764)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 19:13:38 -04:00
comfyanonymous
b7ba504e06
Try to make coderabbit enforce AGENTS.md (#14759)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 14:25:24 -04:00
Silver
6c62ca0b6b
fix: error when embedding is loaded with models using llama_template (#14744)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 17:06:09 +08:00
Varun Chawla
2657155dfc
fix: block Windows drive-qualified paths in path traversal validation
PurePosixPath doesn't understand Windows drive letters, so paths like
C:/Windows/secret.png or C:secret.png would pass validation but escape
the output directory when joined via os.path.join on Windows.

Add PureWindowsPath check to detect drive-qualified paths on all
platforms. Added tests for Windows drive absolute, relative, and
backslash path variants.
2026-03-24 21:39:24 -07:00
Varun Chawla
d29f0228a8
refactor: use PurePosixPath for path traversal validation per review feedback 2026-03-17 22:19:08 -07:00
Varun Chawla
95c511e167
security: handle Windows backslash path traversal in filename validation
Normalize backslashes to forward slashes before checking for path
traversal patterns, preventing attacks like `folder\..\secret` that
bypass forward-slash-only checks on Windows. Addresses review feedback
from light-and-ray on PR #12353.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 22:18:30 -07:00
Varun Chawla
64e1c0a0a6
security: refine path traversal validation to allow consecutive dots in filenames
Fixes #12352

The previous validation incorrectly rejected filenames with consecutive dots
(e.g., test..png) by checking if '..' exists anywhere in the filename.

This commit refines the validation to:
- Block actual path traversal patterns: '../', '/..'
- Block filenames starting with '..' (e.g., '..secret')
- Block absolute paths starting with '/'
- Allow consecutive dots in filenames (e.g., 'test..png', 'my...file.jpg')

Changes:
- Updated validation logic in /view and /upload/mask endpoints
- Added comprehensive test suite covering both security and functionality
- All tests pass: blocks path traversal, allows valid filenames with dots
2026-03-17 22:18:02 -07:00
8 changed files with 254 additions and 29 deletions

View File

@ -4,12 +4,12 @@ early_access: false
tone_instructions: "Only comment on issues introduced by this PR's changes. Do not flag pre-existing problems in moved, re-indented, or reformatted code."
reviews:
profile: "chill"
request_changes_workflow: false
profile: "assertive"
request_changes_workflow: true
high_level_summary: false
poem: false
review_status: false
review_details: false
review_details: true
commit_status: true
collapse_walkthrough: true
changed_files_summary: false
@ -39,6 +39,14 @@ reviews:
- path: "**"
instructions: |
IMPORTANT: Only comment on issues directly introduced by this PR's code changes.
Treat AGENTS.md as mandatory repository policy, not optional style guidance.
Flag PR changes that violate AGENTS.md even when the code is otherwise functional.
In particular, enforce architecture boundaries, dtype/device/memory rules,
interface contracts, import style, no unnecessary try/except blocks, no inline
imports, no outbound internet paths in core ComfyUI, and narrow scoped fixes.
Prefer direct findings over suggestions when a rule is violated. Only ignore
AGENTS.md when it clearly conflicts with a newer explicit maintainer instruction
in the PR.
Do NOT flag pre-existing issues in code that was merely moved, re-indented,
de-indented, or reformatted without logic changes. If code appears in the diff
only due to whitespace or structural reformatting (e.g., removing a `with:` block),
@ -123,5 +131,10 @@ chat:
knowledge_base:
opt_out: false
code_guidelines:
enabled: true
filePatterns:
- files: "AGENTS.md"
applyTo: "**"
learnings:
scope: "auto"

View File

@ -543,18 +543,24 @@ class SDTokenizer:
def _try_get_embedding(self, embedding_name:str):
'''
Takes a potential embedding name and tries to retrieve it.
Returns a Tuple consisting of the embedding and any leftover string, embedding can be None.
Returns a Tuple consisting of the embedding, the cleaned embedding name, and any leftover string, embedding can be None.
'''
split_embed = embedding_name.split()
embedding_name = split_embed[0]
leftover = ' '.join(split_embed[1:])
match = re.search(r'[<\[]', embedding_name)
if match is not None:
leftover = embedding_name[match.start():] + (" " + leftover if leftover else "")
embedding_name = embedding_name[:match.start()]
embed = load_embed(embedding_name, self.embedding_directory, self.embedding_size, self.embedding_key)
if embed is None:
stripped = embedding_name.strip(',')
if len(stripped) < len(embedding_name):
embed = load_embed(stripped, self.embedding_directory, self.embedding_size, self.embedding_key)
return (embed, "{} {}".format(embedding_name[len(stripped):], leftover))
return (embed, leftover)
return (embed, embedding_name, "{} {}".format(embedding_name[len(stripped):], leftover))
return (embed, embedding_name, leftover)
def pad_tokens(self, tokens, amount):
if self.pad_left:
@ -585,7 +591,7 @@ class SDTokenizer:
tokens = []
for weighted_segment, weight in parsed_weights:
to_tokenize = unescape_important(weighted_segment)
split = re.split(' {0}|\n{0}'.format(self.embedding_identifier), to_tokenize)
split = re.split(r'(?<=\s){}'.format(re.escape(self.embedding_identifier)), to_tokenize)
to_tokenize = [split[0]]
for i in range(1, len(split)):
to_tokenize.append("{}{}".format(self.embedding_identifier, split[i]))
@ -595,7 +601,7 @@ class SDTokenizer:
# if we find an embedding, deal with the embedding
if word.startswith(self.embedding_identifier) and self.embedding_directory is not None:
embedding_name = word[len(self.embedding_identifier):].strip('\n')
embed, leftover = self._try_get_embedding(embedding_name)
embed, embedding_name, leftover = self._try_get_embedding(embedding_name)
if embed is None:
logging.warning(f"warning, embedding:{embedding_name} does not exist, ignoring")
else:

View File

@ -937,22 +937,41 @@ class BaseGenerate:
return torch.argmax(logits, dim=-1, keepdim=True)
# Sampling mode
if repetition_penalty != 1.0:
for i in range(logits.shape[0]):
for token_id in set(token_history):
logits[i, token_id] *= repetition_penalty if logits[i, token_id] < 0 else 1/repetition_penalty
if presence_penalty is not None and presence_penalty != 0.0:
for i in range(logits.shape[0]):
for token_id in set(token_history):
logits[i, token_id] -= presence_penalty
if len(token_history) > 0 and (repetition_penalty != 1.0 or (presence_penalty is not None and presence_penalty != 0.0)):
token_ids = torch.tensor(list(set(token_history)), device=logits.device)
token_logits = logits[:, token_ids]
if repetition_penalty != 1.0:
token_logits = torch.where(token_logits < 0, token_logits * repetition_penalty, token_logits / repetition_penalty)
if presence_penalty is not None and presence_penalty != 0.0:
token_logits = token_logits - presence_penalty
logits[:, token_ids] = token_logits
if temperature != 1.0:
logits = logits / temperature
if top_k > 0:
indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
logits[indices_to_remove] = torch.finfo(logits.dtype).min
top_k = min(top_k, logits.shape[-1])
logits, top_indices = torch.topk(logits, top_k)
if min_p > 0.0:
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)
top_probs, _ = probs_before_filter.max(dim=-1, keepdim=True)
min_threshold = min_p * top_probs
indices_to_remove = probs_before_filter < min_threshold
logits[indices_to_remove] = torch.finfo(logits.dtype).min
if top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.cumsum(torch.nn.functional.softmax(sorted_logits, dim=-1), dim=-1)
sorted_indices_to_remove = cumulative_probs > top_p
sorted_indices_to_remove[..., 0] = False
indices_to_remove = torch.zeros_like(logits, dtype=torch.bool)
indices_to_remove.scatter_(1, sorted_indices, sorted_indices_to_remove)
logits[indices_to_remove] = torch.finfo(logits.dtype).min
probs = torch.nn.functional.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1, generator=generator)
return top_indices.gather(1, next_token)
if min_p > 0.0:
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)

View File

@ -9,6 +9,7 @@ from typing import Any
import folder_paths
logger = logging.getLogger(__name__)
_SENSITIVE_HEADERS = {"authorization", "x-api-key"}
def get_log_directory():
@ -73,6 +74,10 @@ def _format_data_for_logging(data: Any) -> str:
return str(data)
def _redact_headers(headers: dict) -> dict:
return {k: ("***" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()}
def log_request_response(
operation_id: str,
request_method: str,
@ -101,7 +106,7 @@ def log_request_response(
log_content.append(f"Method: {request_method}")
log_content.append(f"URL: {request_url}")
if request_headers:
log_content.append(f"Headers:\n{_format_data_for_logging(request_headers)}")
log_content.append(f"Headers:\n{_format_data_for_logging(_redact_headers(request_headers))}")
if request_params:
log_content.append(f"Params:\n{_format_data_for_logging(request_params)}")
if request_data is not None:

View File

@ -16,23 +16,30 @@ class ColorToRGBInt(io.ComfyNode):
],
outputs=[
io.Int.Output(display_name="rgb_int"),
io.Color.Output(display_name="hex")
io.Color.Output(display_name="hex"),
io.Float.Output(display_name="alpha"),
],
)
@classmethod
def execute(cls, color: str) -> io.NodeOutput:
# expect format #RRGGBB
if len(color) != 7 or color[0] != "#":
raise ValueError("Color must be in format #RRGGBB")
# expect format #RRGGBB or #RRGGBBAA
if len(color) not in (7, 9) or color[0] != "#":
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA")
try:
int(color[1:], 16)
except ValueError:
raise ValueError("Color must be in format #RRGGBB") from None
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA") from None
alpha = 1.0
if len(color) == 9:
alpha = int(color[7:9], 16) / 255.0
color = color[:7]
r, g, b = hex_to_rgb(color)
rgb_int = r * 256 * 256 + g * 256 + b
return io.NodeOutput(rgb_int, color)
return io.NodeOutput(rgb_int, color, alpha)
class ColorExtension(ComfyExtension):

View File

@ -1,6 +1,6 @@
comfyui-frontend-package==1.45.20
comfyui-workflow-templates==0.11.2
comfyui-embedded-docs==0.5.6
comfyui-embedded-docs==0.5.7
torch
torchsde
torchvision

View File

@ -2,6 +2,7 @@ import errno
import os
import sys
import asyncio
from pathlib import PurePosixPath, PureWindowsPath
import traceback
import time
@ -475,7 +476,11 @@ class PromptServer():
return web.Response(status=400)
# validation for security: prevent accessing arbitrary path
if filename[0] == '/' or '..' in filename:
# Normalize backslashes and use standard library to parse path components
normalized = filename.replace('\\', '/')
path = PurePosixPath(normalized)
win_path = PureWindowsPath(normalized)
if path.is_absolute() or win_path.is_absolute() or win_path.drive or '..' in path.parts:
return web.Response(status=400)
if output_dir is None:
@ -532,7 +537,10 @@ class PromptServer():
return web.Response(status=400)
# validation for security: prevent accessing arbitrary path
if filename[0] == '/' or '..' in filename:
# Normalize backslashes and use standard library to parse path components
normalized = filename.replace('\\', '/')
path = PurePosixPath(normalized)
if path.is_absolute() or '..' in path.parts:
return web.Response(status=400)
if output_dir is None:

View File

@ -0,0 +1,167 @@
"""Tests for /view endpoint path traversal validation"""
import pytest
from aiohttp import web
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
import os
import tempfile
from pathlib import Path, PurePosixPath, PureWindowsPath
class TestViewEndpointSecurity(AioHTTPTestCase):
"""Test /view endpoint path traversal validation"""
async def get_application(self):
"""Create a minimal test application with /view endpoint"""
app = web.Application()
# Create a test directory with test files
self.test_dir = tempfile.mkdtemp()
self.test_file_valid = os.path.join(self.test_dir, "test..png")
self.test_file_normal = os.path.join(self.test_dir, "normal.png")
self.test_file_dotsecret = os.path.join(self.test_dir, "..secret")
# Create test files
Path(self.test_file_valid).touch()
Path(self.test_file_normal).touch()
Path(self.test_file_dotsecret).touch()
async def view_handler(request):
"""Simplified /view endpoint handler for testing"""
if "filename" not in request.rel_url.query:
return web.Response(status=404)
filename = request.rel_url.query["filename"]
if not filename:
return web.Response(status=400)
# validation for security: prevent accessing arbitrary path
# Normalize backslashes and use standard library to parse path components
normalized = filename.replace('\\', '/')
path = PurePosixPath(normalized)
win_path = PureWindowsPath(normalized)
if path.is_absolute() or win_path.is_absolute() or win_path.drive or '..' in path.parts:
return web.Response(status=400)
# For testing, just check if file exists in test directory
filename = os.path.basename(filename)
file_path = os.path.join(self.test_dir, filename)
if os.path.isfile(file_path):
return web.Response(status=200, text="OK")
return web.Response(status=404)
app.router.add_get('/view', view_handler)
return app
async def tearDownAsync(self):
"""Clean up test files"""
import shutil
if hasattr(self, 'test_dir') and os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
@unittest_run_loop
async def test_allows_consecutive_dots_in_filename(self):
"""Test that files with consecutive dots (e.g., test..png) are allowed"""
resp = await self.client.request("GET", "/view?filename=test..png")
assert resp.status == 200, "Should allow files with consecutive dots in filename"
@unittest_run_loop
async def test_allows_normal_filenames(self):
"""Test that normal filenames work"""
resp = await self.client.request("GET", "/view?filename=normal.png")
assert resp.status == 200, "Should allow normal filenames"
@unittest_run_loop
async def test_blocks_path_traversal_dotdot_slash(self):
"""Test that path traversal with ../ is blocked"""
resp = await self.client.request("GET", "/view?filename=../etc/passwd")
assert resp.status == 400, "Should block ../ path traversal"
@unittest_run_loop
async def test_blocks_path_traversal_slash_dotdot(self):
"""Test that path traversal with /.. is blocked"""
resp = await self.client.request("GET", "/view?filename=folder/../etc/passwd")
assert resp.status == 400, "Should block /.. path traversal"
@unittest_run_loop
async def test_blocks_absolute_paths(self):
"""Test that absolute paths starting with / are blocked"""
resp = await self.client.request("GET", "/view?filename=/etc/passwd")
assert resp.status == 400, "Should block absolute paths"
@unittest_run_loop
async def test_allows_dotdot_prefix_in_filename(self):
"""Test that filenames starting with .. but not as a path component are allowed"""
resp = await self.client.request("GET", "/view?filename=..secret")
assert resp.status == 200, "Should allow filenames starting with .. that aren't path traversal"
@unittest_run_loop
async def test_blocks_bare_dotdot(self):
"""Test that bare .. as a path component is blocked"""
resp = await self.client.request("GET", "/view?filename=..")
assert resp.status == 400, "Should block bare .. path component"
@unittest_run_loop
async def test_multiple_consecutive_dots(self):
"""Test that multiple consecutive dots in filename are allowed"""
# Create a file with multiple dots
test_file = os.path.join(self.test_dir, "test...png")
Path(test_file).touch()
resp = await self.client.request("GET", "/view?filename=test...png")
assert resp.status == 200, "Should allow files with multiple consecutive dots"
@unittest_run_loop
async def test_dots_in_middle_of_filename(self):
"""Test that dots in the middle of filename are allowed"""
# Create a file with dots in middle
test_file = os.path.join(self.test_dir, "my..file..name.png")
Path(test_file).touch()
resp = await self.client.request("GET", "/view?filename=my..file..name.png")
assert resp.status == 200, "Should allow dots in middle of filename"
@unittest_run_loop
async def test_blocks_backslash_path_traversal(self):
"""Test that Windows-style backslash path traversal (\\..) is blocked"""
resp = await self.client.request("GET", "/view?filename=folder%5C..%5Csecret")
assert resp.status == 400, "Should block backslash path traversal"
@unittest_run_loop
async def test_blocks_backslash_dotdot_at_start(self):
"""Test that backslash path traversal starting with ..\\ is blocked"""
resp = await self.client.request("GET", "/view?filename=..%5Cetc%5Cpasswd")
assert resp.status == 400, "Should block ..\\ path traversal at start"
@unittest_run_loop
async def test_blocks_mixed_slash_backslash_traversal(self):
"""Test that mixed forward/backslash path traversal is blocked"""
resp = await self.client.request("GET", "/view?filename=folder/..%5Csecret")
assert resp.status == 400, "Should block mixed slash/backslash path traversal"
@unittest_run_loop
async def test_blocks_backslash_absolute_path(self):
"""Test that Windows absolute paths with backslash are blocked (e.g., C:\\)"""
resp = await self.client.request("GET", "/view?filename=%5Cetc%5Cpasswd")
assert resp.status == 400, "Should block backslash absolute paths"
@unittest_run_loop
async def test_blocks_windows_drive_absolute_path(self):
"""Test that Windows drive-qualified absolute paths (C:/...) are blocked"""
resp = await self.client.request("GET", "/view?filename=C:/Windows/secret.png")
assert resp.status == 400, "Should block Windows drive-qualified absolute paths"
@unittest_run_loop
async def test_blocks_windows_drive_relative_path(self):
"""Test that Windows drive-qualified relative paths (C:secret.png) are blocked"""
resp = await self.client.request("GET", "/view?filename=C:secret.png")
assert resp.status == 400, "Should block Windows drive-qualified relative paths"
@unittest_run_loop
async def test_blocks_windows_drive_backslash_path(self):
"""Test that Windows drive paths with backslashes are blocked"""
resp = await self.client.request("GET", "/view?filename=C:%5CWindows%5Csecret.png")
assert resp.status == 400, "Should block Windows drive backslash paths"