From 5f2387f756180ef9769c2967301af77b786fd981 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:26:56 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20API=20=ED=82=A4=20=EC=9D=B8=EC=A6=9D=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B1=B0=EB=B6=80(DoS)=20?= =?UTF-8?q?=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `x-api-key` 헤더를 통해 비-ASCII 문자가 주입될 경우 `hmac.compare_digest`에서 `TypeError`가 발생하여 어플리케이션 크래시를 유발하는 서비스 거부(DoS) 취약점을 수정했습니다. 비교되는 문자열들을 `utf-8` 바이트 객체로 인코딩하여 오류 없이 비교되도록 구현했습니다. --- .jules/sentinel.md | 5 +++++ saas_web.py | 3 ++- tests/test_saas_web.py | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..739bb107 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-22 - [Sentinel: FastAPI `hmac.compare_digest` 비-ASCII DoS 취약점 수정] +**취약점:** 비-ASCII 문자가 포함된 `hmac.compare_digest` 문자열 비교로 인한 서비스 거부(DoS) 취약점 (CWE-400). +**학습:** `hmac.compare_digest()`는 비-ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시킵니다. 공격자가 HTTP 헤더(`x-api-key`) 등을 통해 악의적인 비-ASCII 문자열을 주입하면, 이를 처리하지 못하고 예외가 발생하여 서버 크래시(HTTP 500) 및 서비스 거부를 초래할 수 있습니다. +**예방:** 사용자 입력값과 설정된 키를 `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..f3dfedc3 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1223,5 +1223,31 @@ def test_video_content_type_accepted_by_validator(self): ) +@unittest.skipUnless( + _HAS_FASTAPI, "fastapi not installed (optional integration dependency)" +) +class TestSaasWebAuth(unittest.IsolatedAsyncioTestCase): + async def test_require_api_key_non_ascii_dos_prevention(self): + from starlette.requests import Request + from starlette.responses import JSONResponse + import json + + with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}): + scope = { + "type": "http", + "method": "POST", + "path": "/api/upload", + "headers": [(b"x-api-key", "ö".encode("utf-8"))], + } + request = Request(scope) + + async def mock_call_next(req): + return JSONResponse(status_code=200, content={"status": "ok"}) + + response = await saas_web.require_api_key(request, mock_call_next) + self.assertEqual(response.status_code, 401) + self.assertEqual(json.loads(response.body), {"error": "Invalid or missing API key"}) + + if __name__ == "__main__": unittest.main()