diff --git a/saas_web.py b/saas_web.py index 63265e9..4c123b8 100644 --- a/saas_web.py +++ b/saas_web.py @@ -114,7 +114,8 @@ 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..0e19dd7 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -707,6 +707,29 @@ def test_missing_header_rejected_when_keys_configured(self): self.assertEqual(response.json(), {"error": "Invalid or missing API key"}) self.assertNotIn("secret-key", response.text) + def test_non_ascii_key_does_not_crash(self): + with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}): + # Using raw ASGI scope to bypass HTTPX ASCII limitations on headers + import fastapi + import asyncio + import json + import saas_web + scope = { + "type": "http", + "method": "POST", + "path": "/shrink", + "headers": [(b"x-api-key", "한글".encode("utf-8"))] + } + request = fastapi.Request(scope) + + async def dummy_call_next(req): + return fastapi.responses.JSONResponse({"success": True}) + + response = asyncio.run(saas_web.require_api_key(request, dummy_call_next)) + + self.assertEqual(response.status_code, 401) + self.assertEqual(json.loads(response.body), {"error": "Invalid or missing API key"}) + def test_wrong_key_rejected(self): with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}): response = self._post_shrink(headers={"X-API-Key": "wrong-key"})