diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..35622938 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -65,3 +65,7 @@ **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-10-04 - [Sentinel: Unhandled TypeError in hmac.compare_digest] +**Vulnerability:** Denial of Service (DoS) vulnerability due to unhandled `TypeError` when `hmac.compare_digest` is called with strings containing non-ASCII characters. +**Learning:** Python's `hmac.compare_digest` does not support comparing strings with non-ASCII characters and raises a `TypeError` instead of returning `False`. If this occurs during API key validation in a web framework, it can cause an internal server error (500) and crash the request processing. +**Prevention:** Always encode string arguments to bytes (e.g., using `.encode('utf-8')`) before passing them to `hmac.compare_digest` to ensure it operates consistently without crashing on malformed input. diff --git a/saas_web.py b/saas_web.py index 63265e94..071b1419 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..8d0218bd 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1223,5 +1223,19 @@ def test_video_content_type_accepted_by_validator(self): ) +class TestSaasWebAPIKeyDos(unittest.TestCase): + @patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "test_key"}, clear=True) + def test_api_key_with_non_ascii_chars_returns_401(self): + # We must pass the raw bytes for headers if they contain non-ASCII + # to simulate how Starlette parses headers and to reach the API key check logic + response = client.post( + "/jobs", + headers=[(b"x-api-key", "한글키".encode("utf-8"))], + files={"file": ("test.wav", io.BytesIO(b"data"), "audio/wav")}, + data={"target_bytes": 1000}, + ) + self.assertEqual(response.status_code, 401) + + if __name__ == "__main__": unittest.main()