Gate-5 on PR #205 (sos-205-f1a3aee4-gate5.md) found the BLOCK-A shape — hmac.compare_digest(str, str) on a value that comes off the wire — alive in the MCP SSE server, on a path that is easier to reach than the one BLOCK-A described.
The defect
hmac.compare_digest on two str raises TypeError on any non-ASCII codepoint; == never raises. Where a wire-supplied string is compared this way, one crafted byte turns a safe compare into an unauthenticated HTTP 500.
sos/mcp/sos_mcp_sse.py:6177 — identical shape.
- Reachable via a
?token= query parameter, not just a header. This matters: the header variant needed a raw socket (httpx/starlette TestClient refuse to encode non-ASCII headers client-side, which is why CI was structurally blind to it). A query param carries UTF-8 natively, so ordinary clients hit this — no latin-1 trick required.
sos/mcp/sos_mcp_sse.py:7439 is the signup funnel — same shape, externally reachable.
Fails closed (no auth bypass), but it is pre-auth, cheaper than presenting a real bad token (the compare runs before any bcrypt/DB work), un-throttleable, and log-floods on demand.
Fix
Compare bytes at every network boundary:
hmac.compare_digest(presented.encode("utf-8"), expected.encode("utf-8"))
guarded by a non-empty check on the expected value so an unset secret can't match an empty presentation (the P0-A fail-open pattern closed in #205).
Related, same class
Standing pattern
A fix that lands in one service and not its siblings leaves the class alive. Grep the whole repo for the exact shape of any fix before calling it done — compare_digest( with any header/query/body-derived str argument, and getenv(NAME, "") feeding an == on an auth path.
Testing note
A green suite is not evidence for the header variant — instrument with a raw socket against a real uvicorn, not TestClient. The query-param variant is testable normally, so it should get an ordinary regression test.
Gate-5 on PR #205 (
sos-205-f1a3aee4-gate5.md) found the BLOCK-A shape —hmac.compare_digest(str, str)on a value that comes off the wire — alive in the MCP SSE server, on a path that is easier to reach than the one BLOCK-A described.The defect
hmac.compare_digeston twostrraisesTypeErroron any non-ASCII codepoint;==never raises. Where a wire-supplied string is compared this way, one crafted byte turns a safe compare into an unauthenticated HTTP 500.sos/mcp/sos_mcp_sse.py:6177— identical shape.?token=query parameter, not just a header. This matters: the header variant needed a raw socket (httpx/starletteTestClientrefuse to encode non-ASCII headers client-side, which is why CI was structurally blind to it). A query param carries UTF-8 natively, so ordinary clients hit this — no latin-1 trick required.sos/mcp/sos_mcp_sse.py:7439is the signup funnel — same shape, externally reachable.Fails closed (no auth bypass), but it is pre-auth, cheaper than presenting a real bad token (the compare runs before any bcrypt/DB work), un-throttleable, and log-floods on demand.
Fix
Compare bytes at every network boundary:
guarded by a non-empty check on the expected value so an unset secret can't match an empty presentation (the P0-A fail-open pattern closed in #205).
Related, same class
sos/services/docs/app.py:125—is_system = raw_token == _SYSTEM_TOKENwith_SYSTEM_TOKEN = os.getenv("SOS_DOCS_TOKEN", ""); a bareAuthorization: Beareryieldsis_system=Truewhen unset. Filed as docs service: system token fails open when SOS_DOCS_TOKEN unset #207, still open.sos/services/squad/auth.py:337,sos/kernel/auth.py:179.Standing pattern
A fix that lands in one service and not its siblings leaves the class alive. Grep the whole repo for the exact shape of any fix before calling it done —
compare_digest(with any header/query/body-derivedstrargument, andgetenv(NAME, "")feeding an==on an auth path.Testing note
A green suite is not evidence for the header variant — instrument with a raw socket against a real uvicorn, not
TestClient. The query-param variant is testable normally, so it should get an ordinary regression test.