diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..314f7547 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. +## 2024-08-16 - hmac.compare_digest의 유니코드 문자열 처리 취약점 +**취약점:** `hmac.compare_digest`에 비ASCII 문자열(예: 이모지)을 포함한 값을 전달할 경우 `TypeError`가 발생하여 500 Server Error DoS 공격을 유발할 수 있음. +**학습:** 파이썬의 `hmac.compare_digest`는 비ASCII 문자를 포함한 문자열 비교를 지원하지 않아 예외를 발생시킨다. 악의적인 사용자가 이를 이용해 잘못된 헤더 값을 전송하여 서버를 강제로 종료시키거나 장애를 유발할 수 있다. +**예방:** `hmac.compare_digest`를 호출하기 전에 항상 두 인자를 명시적으로 바이트(`.encode('utf-8')`)로 인코딩하여 비교해야 한다. diff --git a/saas_web.py b/saas_web.py index 63265e94..071b1419 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 3b57e033..eb706b0f 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1222,6 +1222,25 @@ def test_video_content_type_accepted_by_validator(self): ) ) + @patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "test-key"}) + def test_unicode_api_key_dos(self): + from starlette.requests import Request + import asyncio + async def mock_call_next(request): + return "SUCCESS" + scope = { + 'type': 'http', + 'method': 'POST', + 'path': '/shrink', + 'headers': [(b'x-api-key', 'test-key😀'.encode('utf-8'))], + } + req = Request(scope) + try: + res = asyncio.run(saas_web.require_api_key(req, mock_call_next)) + self.assertEqual(res.status_code, 401) + except Exception as e: + self.fail(f"Middleware crashed with exception: {e}") + if __name__ == "__main__": unittest.main()