Skip to content
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2026-09-04 - [API Key Unicode Support Fix]
**Vulnerability:** The API key validation used `hmac.compare_digest` with unencoded strings, which raises a `TypeError` when given non-ASCII characters, causing a 500 server error and potential DoS vectors.
**Learning:** Python's `hmac.compare_digest` strictly requires strings to contain only ASCII characters. When processing arbitrary user input like HTTP headers, it must be encoded to bytes first.
**Prevention:** Always encode user-provided strings (e.g. `utf-8`) before using them with `hmac.compare_digest`.

## 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.
Expand Down
2 changes: 1 addition & 1 deletion saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ def test_get_ui(self):
self.assertEqual(response.status_code, 200)
self.assertIn(b"Codec Carver SaaS", response.content)

@patch("saas_web.get_configured_api_keys", return_value=["valid-key"])
def test_require_api_key_handles_non_ascii(self, _mock):
from fastapi import Request
scope = {'type': 'http', 'method': 'POST', 'path': '/jobs', 'headers': [(b'x-api-key', '\uc548\ub155'.encode('utf-8'))]}
request = Request(scope)
async def call_next(req):
return None
response = asyncio.run(saas_web.require_api_key(request, call_next))
self.assertEqual(response.status_code, 401)
self.assertEqual(json.loads(response.body.decode()), {"error": "Invalid or missing API key"})

def test_get_ui_includes_accessible_file_input_helpers(self):
response = client.get("/")
self.assertEqual(response.status_code, 200)
Expand Down
Loading