HTTP transport: a bearer value with a non-ASCII byte crashes the shared-secret check with a 500 instead of a 401 #14
Labels
No labels
bug
documentation
enhancement
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-mcp#14
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo-mcp at
0b29e70b9, reading the shared-secret check and the MCP SDK's bearer middleware it runs under.Related to #3.
SharedSecret.verify_tokencompares the presented token with the configured secret usinghmac.compare_digeston twostrvalues.compare_digestrefuses to compare strings that contain anything outside ASCII and raisesTypeErrorinstead of answering. Starlette decodes header bytes as latin-1 and h11 accepts bytes 0x80–0xFF in a header value, so a bearer value carrying such a byte reaches the comparison and the request ends as an unhandled exception rather than a refusal.src/postulo_mcp/serving.py:73-76(SharedSecret.verify_token);src/postulo_mcp/serving.py:140-157(plan, which accepts any secret of 16 characters or more)sseorstreamable-httpport withAuthorization: Bearer <value containing a byte above 0x7F>.BearerAuthBackend.authenticate(mcp 2.1.1,mcp/server/auth/middleware/bearer_auth.py:55-58) passes the remainder of the header toverify_tokenwithout atry.compare_digest(token, self._secret)raisesTypeError: comparing strings with non-ASCII characters is not supported(checked on Python 3.14). Starlette'sAuthenticationMiddlewarecatches onlyAuthenticationError, so the error reachesServerErrorMiddleware: the client gets a 500 and the log a traceback.POSTULO_MCP_HTTP_TOKENto a passphrase with an accented letter,plan()accepts it, and then every request that carries a bearer value, the legitimate client's included, fails the same way. The server is unusable and says nothing about why.compare_digest(token.encode("utf-8"), self._secret.encode("utf-8")), which never raises and still refuses a mismatch. Inplan(), refuse a secret that is not printable ASCII with a sentence besideSHORT_SECRET(a header carries latin-1 at best, so a non-ASCII secret cannot be sent reliably); thesecrets.token_urlsafesuggestion already produces one that passes.Done when
tests/test_server.pyhas a test besidetest_the_shared_secret_is_the_only_token_that_opens_the_portthat callsverify_tokenwith a value containing a non-ASCII character and expectsNone, and an end-to-end case intest_the_port_turns_away_whoever_has_not_got_the_secretthat sends such a bearer value as bytes and expects 401; both fail today withTypeError.serving.plan("streamable-http", environment={"POSTULO_MCP_HTTP_TOKEN": "<16+ characters with é>"})raisesPostuloError.