From e63eb5ab2bb780a3bd36d8b0204f73d4d936a916 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:13:54 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20hm?= =?UTF-8?q?ac.compare=5Fdigest=20TypeError=20DoS=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿšจ Severity: HIGH ๐Ÿ’ก Vulnerability: ์•…์˜์ ์ธ HTTP ํ—ค๋” ๊ฐ’(API ํ‚ค)์— ๋น„ ASCII ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ hmac.compare_digest๊ฐ€ TypeError๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋‹ค์šด์‹œํ‚ค๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ DoS ๊ณต๊ฒฉ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๐ŸŽฏ Impact: ์ธ์ฆ์ด ์‹คํŒจํ•˜๋Š” ๋Œ€์‹  ์‹œ์Šคํ…œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์„œ๋น„์Šค ๊ฑฐ๋ถ€(DoS) ์ƒํƒœ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ”ง Fix: ์™ธ๋ถ€ ์ž…๋ ฅ๊ฐ’(API ํ‚ค)์„ hmac.compare_digest๋กœ ๋น„๊ตํ•˜๊ธฐ ์ „์— ๋‘ ๊ฐ’์„ ๋ชจ๋‘ .encode('utf-8', 'replace')๋ฅผ ํ†ตํ•ด ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ”์ดํŠธ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. โœ… Verification: ๋น„ ASCII ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ํ—ค๋” ํŽ˜์ด๋กœ๋“œ๊ฐ€ 401 ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์„œ๋ฒ„ ์˜ค๋ฅ˜๋ฅผ ์ผ์œผํ‚ค์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธ(test_dos_with_non_ascii_headers)๋กœ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. --- .jules/sentinel.md | 4 ++++ saas_web.py | 2 +- tests/test_saas_web.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..f7d60c89 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-10-09 - Unhandled TypeError DoS via hmac.compare_digest +**Vulnerability:** A Denial of Service (DoS) vulnerability existed because `hmac.compare_digest` throws a `TypeError` when supplied with non-ASCII characters, such as when parsing malicious HTTP header values for API keys. +**Learning:** Python's `hmac.compare_digest` strictly requires both arguments to be of the same encoding and fails on non-ASCII string inputs. When evaluating external input (like HTTP headers), unexpected non-ASCII bytes can crash the authentication middleware instead of cleanly denying access. +**Prevention:** Always explicitly encode strings into `utf-8` bytes (using `.encode("utf-8", "replace")`) before performing cryptographic comparisons with `hmac.compare_digest`, ensuring unhandled exceptions are not thrown due to string decoding issues. diff --git a/saas_web.py b/saas_web.py index 63265e94..41bbdaa9 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", "replace"), key.encode("utf-8", "replace")) 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..1528369e 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -770,6 +770,12 @@ def test_empty_entries_are_ignored(self): self.assertEqual(rejected.status_code, 401) + def test_dos_with_non_ascii_headers(self): + with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret"}): + response = self._post_shrink(headers={b"X-API-Key": b"secret\xe9"}) + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json(), {"error": "Invalid or missing API key"}) + def test_only_empty_entries_leave_endpoints_open(self): with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": " , ,"}): response = self._post_shrink()