Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,8 @@
**Vulnerability:** The `_safe_filename` function in `backend/services/attachment_parser.py` used `pathlib.Path().name` to strip directory components from attachment filenames, but failed to normalize backslashes beforehand. This allowed attackers to use Windows-style path separators (e.g., `..\..\upload`) to bypass path validation on POSIX systems.
**Learning:** Checking for traversal sequences using `pathlib.Path().name` may leave the result vulnerable if the input path can contain Windows-style path separators but the program interprets it dynamically or decodes payloads using backslashes, because POSIX `pathlib` treats backslashes as valid filename characters, not separators.
**Prevention:** Always convert backslashes to forward slashes before parsing filenames using `pathlib.Path().name`.

## 2026-08-10 - [Prevent URL-Encoded Path Traversal in Local HTTP Request Validation]
**Vulnerability:** The `validate_local_request_target` function in `backend/core/local_http.py` checked for path traversals (like `..`) but only decoded the segment once using `urllib.parse.unquote`. This allowed attackers to bypass the validation by doubly URL-encoding the payload (e.g., `%252e%252e%252fadmin`).
**Learning:** Checking for traversal sequences on URL segments is insufficient if the input path can contain URL-encoded payloads and is only decoded once. The check could be bypassed since it happens after single decoding, yet the application or storage mechanism may later decode and use the dangerous payload.
**Prevention:** Always recursively decode `urllib.parse.unquote()` on raw input paths before validating to ensure doubly URL-encoded payloads are correctly decoded and caught, with a bounded loop to avoid DoS.
12 changes: 11 additions & 1 deletion backend/core/local_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,18 @@ def validate_local_request_target(
"local request path contains invalid percent encoding"
)
for raw_segment in parsed.path.split("/"):
decoded_segment = raw_segment
try:
decoded_segment = unquote(raw_segment, errors="strict")
for _ in range(10):
next_segment = unquote(decoded_segment, errors="strict")
if next_segment == decoded_segment:
break
decoded_segment = next_segment
else:
if unquote(decoded_segment, errors="strict") != decoded_segment:
raise LocalHTTPValidationError(
"local request path contains excessive percent encoding"
)
except UnicodeDecodeError as exc:
raise LocalHTTPValidationError(
"local request path contains invalid percent encoding"
Expand Down
22 changes: 22 additions & 0 deletions backend/tests/test_local_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,28 @@ def test_local_request_target_rejects_raw_and_encoded_traversal(path: str) -> No
validate_local_request_target(path)


@pytest.mark.parametrize(
"path",
[
"/api/%252e%252e%252fauth/session",
"/api/%25255cadmin",
"/api/%250Aadmin",
],
)
def test_local_request_target_rejects_nested_encoded_unsafe_segments(path: str) -> None:
with pytest.raises(LocalHTTPValidationError, match="traversal|control characters"):
validate_local_request_target(path)


def test_local_request_target_rejects_excessive_percent_encoding_depth() -> None:
nested = "%2e"
for _ in range(11):
nested = nested.replace("%", "%25")

with pytest.raises(LocalHTTPValidationError, match="excessive percent encoding"):
validate_local_request_target(f"/api/{nested}")


@pytest.mark.parametrize(
"path",
[
Expand Down
Loading