From 03c61e90107785001a0efde23a7d7f75e605738d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:54:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20=EB=B9=84?= =?UTF-8?q?=20ASCII=20API=20=ED=82=A4=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20500?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84=20=EC=98=A4=EB=A5=98(DoS)=20=EC=B7=A8?= =?UTF-8?q?=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 `hmac.compare_digest` 함수가 비 ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시키는 문제를 수정했습니다. 이를 악용하면 악의적인 `X-API-Key` 헤더 전송 시 500 내부 서버 오류가 발생하여 DoS 공격(CWE-400)으로 이어질 수 있습니다. `hmac.compare_digest` 호출 시 제공된 키와 설정된 키 모두 `.encode("utf-8")`을 사용하여 명시적으로 바이트로 변환한 후 비교하도록 수정하여 예외 발생을 방지했습니다. --- .jules/sentinel.md | 4 ++++ saas_web.py | 2 +- tests/test_saas_web.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 858d9d4..91e7034 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -60,3 +60,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. +## 2026-07-28 - [Sentinel: 비 ASCII API 키로 인한 500 내부 서버 오류(DoS) 취약점 수정] +**취약점:** `hmac.compare_digest` 함수가 비 ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시키는 문제로 인해, 악의적인 `X-API-Key` 헤더 전송 시 500 내부 서버 오류가 발생하여 DoS 공격(CWE-400)에 악용될 수 있음. +**학습:** 파이썬의 `hmac.compare_digest`는 비 ASCII 문자열 비교를 지원하지 않습니다. HTTP 헤더와 같이 통제되지 않은 사용자 입력에 대해 인코딩 처리 없이 문자열 상태로 직접 비교를 수행하면 예외가 발생할 수 있습니다. +**예방:** `hmac.compare_digest`를 호출하기 전에 항상 두 인자를 명시적으로 바이트 객체로 인코딩(`.encode('utf-8')`)하여 예외 발생을 방지해야 합니다. diff --git a/saas_web.py b/saas_web.py index 3a7b035..a5e6fc5 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 57b879d..65645f7 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1121,6 +1121,24 @@ def test_video_content_type_accepted_by_validator(self): ) ) + def test_api_key_handles_non_ascii_gracefully(self): + from starlette.requests import Request + import asyncio + async def mock_call_next(request: Request): + from starlette.responses import PlainTextResponse + return PlainTextResponse("OK") + + with patch("saas_web.get_configured_api_keys", return_value=["secret1"]): + scope = { + "type": "http", + "method": "GET", + "path": "/jobs/123", + "headers": [(b"x-api-key", "malicious\xff".encode("latin-1"))], + } + request = Request(scope) + response = asyncio.run(saas_web.require_api_key(request, mock_call_next)) + self.assertEqual(response.status_code, 401) + if __name__ == '__main__': unittest.main()