From 1858ad455f55d6095c759cc2ec5aafa6ce6b975a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:14:53 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=B2=98=EB=A6=AC=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=EB=A5=BC=20=ED=86=B5=ED=95=9C=20API=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20DoS=20=EC=B7=A8=EC=95=BD=EC=A0=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hmac.compare_digest()`가 비-ASCII 문자열에 대해 TypeError를 발생시켜 500 오류를 유발하는 문제를 해결하기 위해, 비교 전 키를 utf-8 바이트로 인코딩하도록 수정했습니다. --- .jules/sentinel.md | 5 +++++ saas_web.py | 3 ++- tests/test_saas_web.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..43aba5dc 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-08-20 - [Sentinel: hmac.compare_digest 문자 인코딩 취약점 수정] +**Vulnerability:** 악의적인 비-ASCII 문자가 포함된 헤더를 통해 발생하는 처리되지 않은 예외(TypeError)로 인한 서비스 거부(DoS) 취약점. +**Learning:** Python의 `hmac.compare_digest()` 함수는 비-ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시킵니다. 웹 서버 미들웨어에서 이러한 예외를 적절히 처리하지 않으면 500 서버 오류가 발생하여 서비스 거부 공격의 경로가 될 수 있습니다. +**Prevention:** `hmac.compare_digest()`를 호출하기 전에 항상 비교할 두 문자열을 명시적으로 바이트 형식(`.encode('utf-8')`)으로 인코딩해야 합니다. diff --git a/saas_web.py b/saas_web.py index 63265e94..92585502 100644 --- a/saas_web.py +++ b/saas_web.py @@ -113,8 +113,9 @@ async def require_api_key(request: Request, call_next): configured_keys = get_configured_api_keys() if configured_keys and not (request.method == "GET" and request.url.path == "/"): provided_key = request.headers.get("x-api-key", "") + provided_key_bytes = provided_key.encode("utf-8") if not any( - hmac.compare_digest(provided_key, key) for key in configured_keys + hmac.compare_digest(provided_key_bytes, 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..2658d25f 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1223,5 +1223,34 @@ def test_video_content_type_accepted_by_validator(self): ) +class AuthMiddlewareTests(unittest.IsolatedAsyncioTestCase): + async def test_require_api_key_handles_non_ascii_gracefully(self): + import starlette.requests + import starlette.responses + import os + + scope = { + 'type': 'http', + 'method': 'GET', + 'path': '/jobs/123', + 'headers': [(b'x-api-key', b'invalid_key_\xc3\xb8')], + } + request = starlette.requests.Request(scope) + + async def mock_call_next(req): + return starlette.responses.Response("OK") + + original_keys = os.environ.get('CODEC_CARVER_API_KEYS') + try: + os.environ['CODEC_CARVER_API_KEYS'] = 'validkey1' + response = await saas_web.require_api_key(request, mock_call_next) + self.assertEqual(response.status_code, 401) + finally: + if original_keys is not None: + os.environ['CODEC_CARVER_API_KEYS'] = original_keys + else: + del os.environ['CODEC_CARVER_API_KEYS'] + + if __name__ == "__main__": unittest.main() From 81015fb343a517d9d5fc10270cea29c7f327b9d4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:05:58 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=B2=98=EB=A6=AC=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=EB=A5=BC=20=ED=86=B5=ED=95=9C=20API=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20DoS=20=EC=B7=A8=EC=95=BD=EC=A0=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hmac.compare_digest()`가 비-ASCII 문자열에 대해 TypeError를 발생시켜 500 오류를 유발하는 문제를 해결하기 위해, 비교 전 키를 utf-8 바이트로 인코딩하도록 수정했습니다. 또한 환경에 따라 테스트 시 발생하는 `ModuleNotFoundError: No module named 'starlette'` 문제를 방지하기 위해 `@unittest.skipUnless(_HAS_FASTAPI, ...)` 데코레이터를 추가했습니다. --- tests/test_saas_web.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index 2658d25f..c99db8e1 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1223,6 +1223,9 @@ def test_video_content_type_accepted_by_validator(self): ) +@unittest.skipUnless( + _HAS_FASTAPI, "fastapi not installed (optional integration dependency)" +) class AuthMiddlewareTests(unittest.IsolatedAsyncioTestCase): async def test_require_api_key_handles_non_ascii_gracefully(self): import starlette.requests