From 55d5a92caa06aff00e9d2e50a29fc1b4ea93f139 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:52:16 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20HMAC=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20DoS?= =?UTF-8?q?=20=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 --- .jules/sentinel.md | 4 ++++ saas_web.py | 2 +- tests/test_saas_web.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..284666b6 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. +## 2026-07-15 - [Sentinel: FastAPI HMAC 비 ASCII 문자 예외 처리 취약점] +**Vulnerability:** HMAC 비교 함수(hmac.compare_digest)에 비 ASCII 문자열 전달 시 발생하는 TypeError로 인한 서비스 거부(DoS) 취약점. +**Learning:** `hmac.compare_digest`는 비 ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시킵니다. 사용자가 제공하는 헤더 값(`x-api-key` 등)에 비 ASCII 문자가 포함될 수 있으며, 이를 처리하지 않고 직접 비교할 경우 처리되지 않은 예외가 발생하여 서버 프로세스가 종료되거나 서비스 거부 상태에 빠질 수 있습니다. +**Prevention:** 사용자 제공 문자열과 비교 대상 문자열을 `hmac.compare_digest`에 전달하기 전에 항상 바이트(bytes)로 명시적으로 인코딩해야 합니다. 특히 FastAPI/Starlette는 헤더를 `latin1`으로 디코딩하므로, 제공된 키는 `.encode('latin1', errors='replace')`를 통해 다시 인코딩하여 비교하는 것이 안전합니다. diff --git a/saas_web.py b/saas_web.py index 63265e94..93c573ef 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('latin1', errors='replace'), 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..5215419a 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1223,5 +1223,16 @@ def test_video_content_type_accepted_by_validator(self): ) +class TestHMACUnicodeDoS(unittest.TestCase): + def test_unicode_hmac_dos(self): + from unittest import mock + with mock.patch.dict("os.environ", {"CODEC_CARVER_API_KEYS": "my-secret-key"}): + from saas_web import app + from fastapi.testclient import TestClient + c = TestClient(app) + response = c.post("/jobs", headers=[(b"x-api-key", "한글키".encode("utf-8"))]) + self.assertEqual(response.status_code, 401) + + if __name__ == "__main__": unittest.main() From bdc52c254f9cfc6fc8c2259aa5ca5324c89f45b8 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:57:33 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20HMAC=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20DoS?= =?UTF-8?q?=20=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