Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-01 - [Sentinel: Fix hmac.compare_digest TypeError DoS Vulnerability]
**Vulnerability:** Denial of Service (DoS) via unhandled `TypeError` exceptions.
**Learning:** `hmac.compare_digest` raises a `TypeError` if called with string arguments where either string contains non-ASCII characters. Passing unvalidated user input (like HTTP headers) directly to this function can cause a server crash (500 Internal Server Error) when processing malicious input.
**Prevention:** Always encode string inputs to `utf-8` bytes before passing them to `hmac.compare_digest` to ensure safe, constant-time comparison regardless of character encoding.

## 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
17 changes: 17 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,23 @@ def test_missing_header_rejected_when_keys_configured(self):
self.assertEqual(response.json(), {"error": "Invalid or missing API key"})
self.assertNotIn("secret-key", response.text)

def test_non_ascii_key_rejected_safely(self):
import asyncio
from fastapi import Request
from saas_web import require_api_key

scope = {'type': 'http', 'method': 'POST', 'path': '/shrink', 'headers': [(b'x-api-key', '안녕'.encode('utf-8'))]}
request = Request(scope)

async def call_next(req):
pass

with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}):
response = asyncio.run(require_api_key(request, call_next))
Comment on lines +721 to +722

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Runtime secrets remain environment-backed

The regression test reinforces direct runtime reads from CODEC_CARVER_API_KEYS. Repository governance requires authentication secrets to come from a credential registry.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


self.assertEqual(response.status_code, 401)
self.assertEqual(json.loads(response.body), {"error": "Invalid or missing API key"})

def test_wrong_key_rejected(self):
with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}):
response = self._post_shrink(headers={"X-API-Key": "wrong-key"})
Expand Down
Loading