diff --git a/saas_web.py b/saas_web.py index 63265e9..5b280f0 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 3b57e03..557ba73 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -186,6 +186,24 @@ def test_shrink_media_rejects_nonpositive_target_bytes(self): {"error": "Invalid target_bytes value. Must be greater than 0."}, ) + +@unittest.skipUnless(_HAS_FASTAPI, "fastapi not installed (optional integration dependency)") +class TestAPIKeyDoS(unittest.IsolatedAsyncioTestCase): + async def test_non_ascii_key_does_not_crash(self): + import starlette.requests + from saas_web import require_api_key + with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}): + scope = { + "type": "http", + "method": "POST", + "path": "/shrink", + "headers": [(b"x-api-key", "non-ascii-☃".encode("utf-8"))], + } + request = starlette.requests.Request(scope) + response = await require_api_key(request, lambda r: None) + + self.assertEqual(response.status_code, 401) + def test_shrink_media_rejects_missing_filename(self): response = saas_web.shrink_media( BackgroundTasks(),