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()