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.

## 2025-06-15 - Prevent 500 DoS via Non-ASCII Authentication Headers
**Vulnerability:** The `/parse` API used `hmac.compare_digest(provided, expected)` for bearer token verification. If a user provided a non-ASCII string (e.g., Unicode text) in the `Authorization` header, Python's `hmac.compare_digest` raised a `TypeError`, resulting in an unhandled 500 Internal Server Error instead of a 401 Unauthorized, creating a Denial of Service risk.
**Learning:** Python's `hmac.compare_digest()` only supports comparing byte streams or ASCII-only strings. It cannot natively compare strings that contain non-ASCII characters.
**Prevention:** Always encode strings to bytes (e.g., `.encode('utf-8')`) before passing them to `hmac.compare_digest()` when dealing with untrusted client input.
2 changes: 1 addition & 1 deletion src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def require_authorization(
return
expected = f"Bearer {token}"
provided = authorization or ""
if not hmac.compare_digest(provided, expected):
if not hmac.compare_digest(provided.encode("utf-8"), expected.encode("utf-8")):
raise HTTPException(
status_code=401,
detail=UNAUTHORIZED_DETAIL,
Expand Down
15 changes: 15 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,18 @@ def test_get_api_token_strips_surrounding_whitespace(monkeypatch):

def test_config_module_exposes_env_var_name():
assert config.API_TOKEN_ENV_VAR == "NEWSDOM_API_TOKEN"


def test_parse_rejects_non_ascii_bearer_gracefully(monkeypatch, stub_parser):
"""Ensure hmac.compare_digest does not raise a 500 TypeError on non-ASCII headers."""
monkeypatch.setenv(API_TOKEN_ENV_VAR, "s3cret-token")
client = TestClient(app)
# Use explicit raw byte headers to bypass httpx framework-level string encoding
# before it hits FastAPI
response = client.post(
"/parse",
files=_PDF_FILES,
headers=[(b"Authorization", b"Bearer \xe3\x81\x82")],
)
assert response.status_code == 401
assert response.json()["detail"] == "Unauthorized"
Loading