From 694ddc85290d9d78f1999f96e502231b2beee94d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:13:58 +0000 Subject: [PATCH 1/2] =?UTF-8?q?hmac.compare=5Fdigest=EC=9D=98=20=EB=B9=84?= =?UTF-8?q?=20ASCII=20=EB=AC=B8=EC=9E=90=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=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 | 2 +- test_hmac.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ test_hmac_2.py | 9 ++++++++ test_hmac_3.py | 10 +++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test_hmac.py create mode 100644 test_hmac_2.py create mode 100644 test_hmac_3.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 9c9d083b..ae0f636d 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,3 +1,8 @@ +## 2026-07-28 - [Sentinel: Unhandled Exception in hmac.compare_digest] +**Vulnerability:** Unhandled Exception (CWE-754) leading to 500 Internal Server Error via non-ASCII header injection. +**Learning:** `hmac.compare_digest` throws a `TypeError` when comparing strings containing non-ASCII characters. If client headers are decoded and contain such characters, comparing them against ASCII keys causes the application to crash instead of failing securely (401). +**Prevention:** Always encode strings to `utf-8` bytes before passing them to `hmac.compare_digest` to ensure robust comparison and secure failure for malformed inputs. + ## 2026-07-25 - [Cross-platform upload basename normalization] **Behavior:** Upload metadata now interprets both forward slashes and backslashes as path separators before extracting a basename. **Learning:** On POSIX systems, `pathlib.Path(filename).name` retains backslashes because they are ordinary characters there. That caused inconsistent manifest and converter filenames for Windows-style client paths. The upload itself is still written inside a trusted temporary workspace, and batch archive entry names are generated outputs; this change does not establish a filesystem traversal or archive-entry escape. 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/test_hmac.py b/test_hmac.py new file mode 100644 index 00000000..70d20985 --- /dev/null +++ b/test_hmac.py @@ -0,0 +1,53 @@ +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient +from fastapi.responses import JSONResponse +import hmac +import traceback +import uvicorn +import httpx +import asyncio + +app = FastAPI() + +configured_keys = ["validkey"] + +@app.middleware("http") +async def require_api_key(request: Request, call_next): + provided_key = request.headers.get("x-api-key", "") + try: + if not any(hmac.compare_digest(provided_key, key) for key in configured_keys): + return JSONResponse(status_code=401, content={"error": "Invalid or missing API key"}) + except Exception as e: + print("Exception:", e) + traceback.print_exc() + return JSONResponse(status_code=500, content={"error": "Server error"}) + return await call_next(request) + +@app.get("/test") +def test(): + return {"status": "ok"} + +async def run_test(): + config = uvicorn.Config(app, port=8888, log_level="info") + server = uvicorn.Server(config) + task = asyncio.create_task(server.serve()) + await asyncio.sleep(1) # wait for server to start + + # Use raw socket to bypass httpx ascii check + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("127.0.0.1", 8888)) + + # send raw bytes + req = b"GET /test HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nx-api-key: invalid\xc3\xb1\r\n\r\n" + s.sendall(req) + + resp = s.recv(4096) + print("Response:\n", resp.decode('latin1')) + s.close() + + server.should_exit = True + await task + +if __name__ == "__main__": + asyncio.run(run_test()) diff --git a/test_hmac_2.py b/test_hmac_2.py new file mode 100644 index 00000000..bdf3244b --- /dev/null +++ b/test_hmac_2.py @@ -0,0 +1,9 @@ +import hmac + +provided_key = "invalid\xc3\xb1" # simulated latin-1 decode from ASGI +key = "validkey" + +try: + hmac.compare_digest(provided_key, key) +except Exception as e: + print(e) diff --git a/test_hmac_3.py b/test_hmac_3.py new file mode 100644 index 00000000..baa1b23d --- /dev/null +++ b/test_hmac_3.py @@ -0,0 +1,10 @@ +import hmac + +provided_key = "invalid\xc3\xb1" # simulated latin-1 decode from ASGI +key = "validkey" + +try: + hmac.compare_digest(provided_key.encode('utf-8'), key.encode('utf-8')) + print("Success") +except Exception as e: + print(e) From 50e35b52f9e669fa163f325e77413b66e3fb10ac Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:45:46 +0000 Subject: [PATCH 2/2] =?UTF-8?q?hmac.compare=5Fdigest=EC=9D=98=20=EB=B9=84?= =?UTF-8?q?=20ASCII=20=EB=AC=B8=EC=9E=90=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit