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 @@ -65,3 +65,8 @@
**Vulnerability:** Path traversal in `media_shrinker.py` via unresolved `..` segments or symlink escapes before deriving conversion output paths.
**Learning:** `Path.relative_to()` is only a lexical containment check unless both the source and root have first been resolved into canonical absolute paths. Relative paths and symlinks can otherwise bypass root-boundary assumptions.
**Prevention:** Resolve both source and root once, reject sources outside the resolved root with a sanitized `MediaShrinkerError`, and derive `rel_source` from the resolved paths before planning outputs.

## 2026-07-28 - [Sentinel: FastAPI hmac.compare_digest TypeError DoS]
**Vulnerability:** Unhandled Exception causing Denial of Service (DoS) when comparing non-ASCII strings in `hmac.compare_digest`.
**Learning:** Python's `hmac.compare_digest()` raises a `TypeError` if either argument is a string containing non-ASCII characters. Passing unsanitized user input (like HTTP headers) directly to this function can crash the request handling, leading to a potential DoS vulnerability.
**Prevention:** Always encode user-provided strings and configured secrets to bytes (e.g., using `.encode('utf-8')`) before passing them to `hmac.compare_digest()`.
2 changes: 1 addition & 1 deletion saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def require_api_key(request: Request, call_next):
if configured_keys and not (request.method == "GET" and request.url.path == "/"):
provided_key = request.headers.get("x-api-key", "")
if not any(
hmac.compare_digest(provided_key, key) for key in configured_keys
hmac.compare_digest(provided_key.encode('utf-8'), key.encode('utf-8')) for key in configured_keys

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 UTF-8 API keys fail authentication

When a client sends a configured non-ASCII key as UTF-8 header bytes, require_api_key returns 401. Starlette decodes headers as Latin-1, so re-encoding that text cannot recover the client's bytes.

Learn more

The authentication middleware compares an HTTP header against keys loaded from configuration. Starlette exposes header values as Latin-1-decoded strings, even when a client sends UTF-8 bytes. Encoding that string as UTF-8 changes non-ASCII bytes before the comparison, so the configured UTF-8 key cannot match. The configured key remains a Python Unicode string in get_configured_api_keys.

Example: With a configured key cafΓ©, a client sends X-API-Key as b'caf\xc3\xa9'. Starlette provides café; encoding it yields b'caf\xc3\x83\xc2\xa9', not the configured b'caf\xc3\xa9'. The request returns 401 instead of reaching the endpoint.

Recommended fix: Compare the raw X-API-Key header bytes from request.scope['headers'] against configured keys encoded as UTF-8, preserving the existing missing-header and duplicate-header behavior. Add tests for matching and mismatching non-ASCII configured keys with raw UTF-8 header bytes.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

):
return JSONResponse(
status_code=401,
Expand Down
19 changes: 19 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,25 @@ def test_video_content_type_accepted_by_validator(self):
)
)

class TestSaaSHmacSecurity(unittest.TestCase):
def test_hmac_compare_digest_non_ascii(self):
from unittest import mock
from fastapi.testclient import TestClient
Comment on lines +1225 to +1228

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ” Core-only test runs fail

Without the optional web dependencies, this test raises ModuleNotFoundError instead of skipping. The other web test classes use _HAS_FASTAPI to keep core-only runs viable.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

import saas_web

with mock.patch.dict("os.environ", {"CODEC_CARVER_API_KEYS": "test-key-1, test-key-2"}):
client = TestClient(saas_web.app)

# Test valid request (to ensure endpoint logic doesn't block it initially)
response = client.get("/jobs/123", headers={"X-API-Key": "test-key-1"})
self.assertNotEqual(response.status_code, 401)

# Test request with non-ASCII header
# Using raw bytes tuples to prevent client string decoding errors
headers = [(b"x-api-key", "ν•œκΈ€ν‚€".encode("utf-8"))]
response = client.get("/jobs/123", headers=headers)
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json(), {"error": "Invalid or missing API key"})

if __name__ == "__main__":
unittest.main()
Loading