diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..2cfb3a59 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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()`. diff --git a/saas_web.py b/saas_web.py index 63265e94..5b280f05 100644 --- a/saas_web.py +++ b/saas_web.py @@ -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 ): return JSONResponse( status_code=401, diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index 3b57e033..d08c2216 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -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 + 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()