Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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