Skip to content
Open
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
Expand Up @@ -65,3 +65,8 @@
**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.

## 2026-10-08 - [Sentinel: Unhandled Exception DoS via hmac.compare_digest]
**Vulnerability:** Denial of Service (CWE-400/CWE-754) due to unhandled `TypeError` when comparing non-ASCII API keys using `hmac.compare_digest()`.
**Learning:** Passing strings containing non-ASCII characters directly to Python's `hmac.compare_digest()` raises a `TypeError`. In a FastAPI web application middleware, this results in an unhandled 500 Internal Server Error, allowing an attacker to DoS the application by sending malformed or international characters in the HTTP headers.
**Prevention:** Always encode string arguments to bytes (e.g., using `.encode('latin1')` for Starlette request headers, or `.encode('utf-8')` for general strings) before comparing them with `hmac.compare_digest()`.
4 changes: 3 additions & 1 deletion saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ async def require_api_key(request: Request, call_next):
configured_keys = get_configured_api_keys()
if configured_keys and not (request.method == "GET" and request.url.path == "/"):
provided_key = request.headers.get("x-api-key", "")
# Starlette decodes headers using latin1 (ISO-8859-1) by default as per RFC.
provided_key_bytes = provided_key.encode("latin1", errors="replace")
if not any(
hmac.compare_digest(provided_key, key) for key in configured_keys
hmac.compare_digest(provided_key_bytes, key.encode("utf-8")) for key in configured_keys
):
return JSONResponse(
status_code=401,
Expand Down
16 changes: 16 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,22 @@ def test_video_content_type_accepted_by_validator(self):
)
)

class TestUnicodeAPIKeys(unittest.TestCase):
@patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "ํ•œ๊ธ€ํ‚ค"})
def test_unicode_api_key_handles_safely(self):
from fastapi.testclient import TestClient
import saas_web
client = TestClient(saas_web.app)

# Test valid unicode key
response = client.get("/jobs/missing", headers=[(b"X-API-Key", "ํ•œ๊ธ€ํ‚ค".encode("utf-8"))])
self.assertNotEqual(response.status_code, 500)
self.assertNotEqual(response.status_code, 401)

# Test invalid unicode key
response = client.get("/jobs/missing", headers=[(b"X-API-Key", "ํ‹€๋ฆฐํ‚ค".encode("utf-8"))])
self.assertEqual(response.status_code, 401)


if __name__ == "__main__":
unittest.main()
Loading