From 94a91bcacf142599048a55c0e45c93049676bf52 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:53:36 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20hmac.compare=5Fdigest=20TypeError=20DoS=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=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 | 5 +++++ saas_web.py | 4 +++- tests/test_saas_web.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..796330f1 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. + +## 2026-10-08 - [Sentinel: Unhandled Exception DoS via hmac.compare_digest] +**Vulnerability:** Denial of Service (CWE-400/CWE-754) due to unhandled `TypeError` when comparing non-ASCII API keys using `hmac.compare_digest()`. +**Learning:** Passing strings containing non-ASCII characters directly to Python's `hmac.compare_digest()` raises a `TypeError`. In a FastAPI web application middleware, this results in an unhandled 500 Internal Server Error, allowing an attacker to DoS the application by sending malformed or international characters in the HTTP headers. +**Prevention:** Always encode string arguments to bytes (e.g., using `.encode('latin1')` for Starlette request headers, or `.encode('utf-8')` for general strings) before comparing them with `hmac.compare_digest()`. diff --git a/saas_web.py b/saas_web.py index 63265e94..8067c609 100644 --- a/saas_web.py +++ b/saas_web.py @@ -113,8 +113,10 @@ async def require_api_key(request: Request, call_next): configured_keys = get_configured_api_keys() if configured_keys and not (request.method == "GET" and request.url.path == "/"): provided_key = request.headers.get("x-api-key", "") + # Starlette decodes headers using latin1 (ISO-8859-1) by default as per RFC. + provided_key_bytes = provided_key.encode("latin1", errors="replace") if not any( - hmac.compare_digest(provided_key, key) for key in configured_keys + hmac.compare_digest(provided_key_bytes, 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..cf7486c4 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -1222,6 +1222,22 @@ def test_video_content_type_accepted_by_validator(self): ) ) +class TestUnicodeAPIKeys(unittest.TestCase): + @patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "한글키"}) + def test_unicode_api_key_handles_safely(self): + from fastapi.testclient import TestClient + import saas_web + client = TestClient(saas_web.app) + + # Test valid unicode key + response = client.get("/jobs/missing", headers=[(b"X-API-Key", "한글키".encode("utf-8"))]) + self.assertNotEqual(response.status_code, 500) + self.assertNotEqual(response.status_code, 401) + + # Test invalid unicode key + 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 a4369a5c3013ba1293b3cb9348f4c48da19195b3 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:59:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20hmac.compare=5Fdigest=20TypeError=20DoS=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=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