From 944692dac71e9e570341d76b92eced845e8fa7a7 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:07:24 +0000 Subject: [PATCH 1/2] =?UTF-8?q?API=20=ED=82=A4=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=9D=98=20DoS=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(hmac.compare=5Fdigest)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 5 +++++ saas_web.py | 2 +- tests/test_saas_web.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..31f2868c 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. + +## 2024-10-13 - [DoS Risk via hmac.compare_digest with Non-ASCII Strings] +**Vulnerability:** Sending non-ASCII characters in an HTTP header compared via `hmac.compare_digest` raises an unhandled `TypeError` ("comparing strings with non-ASCII characters is not supported"). This allows a malicious user to crash the worker process or trigger 500 errors by repeatedly sending payloads with non-ASCII characters, resulting in a Denial of Service (DoS). +**Learning:** `hmac.compare_digest` safely compares standard ASCII strings, but strictly rejects Python strings containing non-ASCII characters. +**Prevention:** Always encode user-provided strings (e.g., `.encode('utf-8')`) before comparing them using `hmac.compare_digest` to ensure safe byte-level comparison and prevent exception-based DoS. diff --git a/saas_web.py b/saas_web.py index 63265e94..5b280f05 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..305c5961 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1222,6 +1222,19 @@ def test_video_content_type_accepted_by_validator(self): ) ) +class HMACTest(unittest.TestCase): + def test_non_ascii_header_does_not_crash(self): + import os + from unittest.mock import patch + from fastapi.testclient import TestClient + import saas_web + + client = TestClient(saas_web.app) + with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret"}): + # Pass header as raw bytes to prevent test client decoding errors + response = client.get("/jobs/missing", headers=[(b'x-api-key', '한글키'.encode('utf-8'))]) + self.assertEqual(response.status_code, 401) + if __name__ == "__main__": unittest.main() From 564d985ddc2e6a7a814bde162d1d335781e752e6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:14:24 +0000 Subject: [PATCH 2/2] =?UTF-8?q?API=20=ED=82=A4=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=9D=98=20DoS=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(hmac.compare=5Fdigest)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit