From ec1e4c249bb04ba248767d4f373b4188971cb265 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:33:14 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B9=84=20ASCII=20=EC=9E=85=EB=A0=A5=EC=8B=9C?= =?UTF-8?q?=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20hmac.compare=5Fdigest?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=88=84=EB=9D=BD=20=ED=98=84=EC=83=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 ++++ saas_web.py | 2 +- tests/test_saas_web.py | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..d2a5663f 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. +## 2024-08-12 - API 키 비교 시 비 ASCII 문자열로 인한 500 서버 에러 해결 +**취약점:** `hmac.compare_digest`는 비 ASCII 문자가 포함된 문자열 비교 시 `TypeError`를 발생시켜 DoS(Denial of Service) 공격에 취약함. +**학습:** 파이썬의 `hmac.compare_digest`는 유니코드 문자열을 비교할 때 비 ASCII 문자를 지원하지 않음. 악의적인 사용자가 헤더에 비 ASCII 문자를 포함시키면 서버 에러가 발생함. +**예방:** 항상 `encode("utf-8")`을 사용하여 인자들을 바이트로 변환한 후 비교해야 함. 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..9b0febe2 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -32,6 +32,29 @@ _HAS_FASTAPI, "fastapi not installed (optional integration dependency)" ) class TestSaasWeb(unittest.TestCase): + @patch("saas_web.get_configured_api_keys", return_value=["valid-key"]) + def test_auth_middleware_non_ascii_header(self, mock_get_keys): + # Create a mock Request with a non-ASCII header + from starlette.requests import Request + scope = { + 'type': 'http', + 'method': 'GET', + 'path': '/api/jobs', + 'headers': [(b'x-api-key', 'non-ascii\u2022'.encode('utf-8'))], + } + request = Request(scope) + + import asyncio + import saas_web + + async def call_next(req): + from starlette.responses import JSONResponse + return JSONResponse(content={"status": "ok"}) + + loop = asyncio.get_event_loop() + response = loop.run_until_complete(saas_web.require_api_key(request, call_next)) + self.assertEqual(response.status_code, 401) + def test_get_ui(self): response = client.get("/") self.assertEqual(response.status_code, 200)