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-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.
Expand Down
2 changes: 1 addition & 1 deletion saas_web.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿ“ Info: API keys still sourced from environment

get_configured_api_keys reads CODEC_CARVER_API_KEYS from os.environ, the anti-pattern AGENTS.md marks for migration to the credential registry. Unchanged by this PR and outside the diff, but noted since adjacent auth code is being edited.

(Refers to this code)

Devin Review

Was this helpful? React with ๐Ÿ‘ or ๐Ÿ‘Ž to provide feedback.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿ“ Info: UTF-8 encoding fix resolves the TypeError

require_api_key encodes both operands to bytes before comparison. Starlette decodes headers as latin-1, so the provided key can hold non-ASCII code points; str.encode('utf-8') never raises, removing the prior TypeError. ASCII comparison semantics are unchanged.

Devin Review

Was this helpful? React with ๐Ÿ‘ or ๐Ÿ‘Ž to provide feedback.

):
return JSONResponse(
status_code=401,
Expand Down
53 changes: 53 additions & 0 deletions test_hmac.py
Original file line number Diff line number Diff line change
@@ -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())
Comment on lines +1 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐ŸŸก Debug scratch files break the docstring-coverage gate

Three new root-level scripts (run_test even boots a live uvicorn server) carry no module or function docstrings. The repo mandates 100% interrogate coverage and excludes only scripts/tests/fuzz, so these root files fail the gate.

Prompt for agents
test_hmac.py, test_hmac_2.py, and test_hmac_3.py are manual debugging scripts left over from developing the hmac fix (test_hmac.py starts a real uvicorn server on port 8888 and uses raw sockets). They should not be committed: they add no automated test value (CI only runs unittest discover under tests/), and they violate the repo's 100% docstring-coverage rule since interrogate scans root-level files and only excludes scripts/tests/fuzz. Remove all three files. If a regression test for the non-ASCII API-key case is desired, add it as a proper unittest under tests/ with docstrings, using FastAPI TestClient rather than a live server.
Devin Review

Was this helpful? React with ๐Ÿ‘ or ๐Ÿ‘Ž to provide feedback.

9 changes: 9 additions & 0 deletions test_hmac_2.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions test_hmac_3.py
Original file line number Diff line number Diff line change
@@ -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)
Loading