Skip to content
Open
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 @@ -90,3 +90,8 @@
**Vulnerability:** The `_safe_upload_filename` function used `filename.replace`, `PurePosixPath`, and `re.sub` on unbounded client input, making it vulnerable to ReDoS or CPU/memory exhaustion (DoS) when fed extremely long strings.
**Learning:** Even fast standard library functions like `PurePosixPath` and string replacements can cause significant lag when chained on strings in the megabytes. String processing operations should always bound their inputs first if the input is untrusted and can be arbitrarily large.
**Prevention:** Cap the length of client-provided filename strings early by slicing them (e.g. `filename = filename[-512:]`) before doing more complex string parsing or regex replacements, especially when only the basename suffix is relevant.

## 2026-07-20 - Prevent Timing Attack on Token Length
**Vulnerability:** hmac.compare_digest provides constant-time string comparison but can leak the length of the expected token via short-circuiting or timing differences when provided and expected lengths do not match.
**Learning:** Relying solely on hmac.compare_digest without first masking length differences can expose the length of sensitive secrets, facilitating subsequent offline attacks.
**Prevention:** Use a branchless length check and construct a dummy check token of the correct length if a mismatch occurs, ensuring the digest comparison itself always operates on strings of identical length.
7 changes: 6 additions & 1 deletion src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ def _parse_access_failure(request: Request) -> JSONResponse | None:
scheme, separator, credentials = provided.partition(b" ")
if separator != b" " or scheme.lower() != b"bearer" or not credentials:
return _unauthorized_response()
if not hmac.compare_digest(credentials, token.encode("utf-8")):

# Mitigate timing attacks that could leak the length of the expected token
expected = token.encode("utf-8")
valid_length = len(credentials) == len(expected)
check_token = credentials if valid_length else expected
if not (hmac.compare_digest(check_token, expected) and valid_length):
return _unauthorized_response()
return None

Expand Down
Loading