security(dci): reject non-ASCII bearer tokens with 401 (avoid auth 500s)#325
Draft
gonzalesedwin1123 wants to merge 5 commits into
Draft
security(dci): reject non-ASCII bearer tokens with 401 (avoid auth 500s)#325gonzalesedwin1123 wants to merge 5 commits into
gonzalesedwin1123 wants to merge 5 commits into
Conversation
A Bearer token carrying non-ASCII header bytes reaches hmac.compare_digest as a non-ASCII str, raising TypeError -> unhandled 500 on public DCI endpoints. Add regression tests (currently failing) pinning a 401 for both the configured-token and empty-list opt-out paths.
…t-time compare Header bytes are latin-1-decoded, so a non-ASCII Authorization value reaches verify_bearer_token as a non-ASCII str. hmac.compare_digest raises TypeError on non-ASCII str operands; the bearer dependency has no try/except, so the error escaped as a generic 500 with a stack trace instead of a 401. Reject non-ASCII credentials up front (before the constant-time loop, the OAuth2 JWT path, and the opt-out return). No legitimate bearer credential is non-ASCII, so this changes no valid-caller behaviour and preserves the timing-safe comparison for ASCII tokens. Regression from the switch to hmac.compare_digest for constant-time token comparison.
Patch bump 19.0.2.0.3 -> 19.0.2.0.4 with a HISTORY fragment. README.rst and static/description/index.html are regenerated from CI's pinned generator.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #325 +/- ##
==========================================
+ Coverage 74.28% 75.20% +0.92%
==========================================
Files 372 173 -199
Lines 25385 12334 -13051
==========================================
- Hits 18857 9276 -9581
+ Misses 6528 3058 -3470
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Generated files from CI's pinned oca-gen-addon-readme output (applied verbatim from the CI diff), matching the HISTORY.md fragment.
Control characters are ASCII and pass the non-ASCII guard, reaching hmac.compare_digest without crashing. Document that the guard rejects only non-ASCII input and that a control-char token is a normal 401 non-match. Raised in staff review of the non-ASCII bearer fix.
This was referenced Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
spp_dci_server/middleware/signature.py::verify_bearer_tokenpasses the rawAuthorizationheader token straight tohmac.compare_digest(token, candidate)when static tokens are configured (
dci.api_tokensnon-empty).HTTP headers are decoded as latin-1, so a request whose Bearer token contains
non-ASCII bytes reaches this code as a non-ASCII
str.hmac.compare_digestaccepts
stroperands only when they are ASCII — a non-ASCII operand raisesTypeError: comparing strings with non-ASCII characters is not supported.verify_bearer_tokenhas notry/exceptaround the compare, andTypeErrorisnot a
DCIHTTPException. As a FastAPI dependency it runs before the routebody, so the routers' own
try/except → 500wrappers don't cover it — the errorescapes as a generic 500 and logs a stack trace on every bearer-authenticated
DCI endpoint (
search,bulk_upload,async,receipt,callbacks,registry_aliases,ping). An unauthenticated caller can trigger it at will.Regression from the switch to
hmac.compare_digestfor constant-time tokencomparison; the prior
token in accepted_tokensmembership test handled suchinput as an ordinary non-match (clean 401). Severity: low/medium — an
unauthenticated error/DoS-flavoured path plus log noise; no data exposure or
auth bypass.
Fix
Reject non-ASCII credentials with a 401 before any comparison, placed right
after the empty-token check so the single guard covers the constant-time loop,
the OAuth2-JWT path, and the empty-list opt-out return:
No legitimate bearer credential is ever non-ASCII (OAuth2 JWTs are base64url;
configured static tokens are ASCII), so this changes no valid-caller behaviour
and preserves the timing-safe comparison for ASCII tokens. Reuses the existing
err.auth.invalid_tokencode (a non-ASCII token is, semantically, just invalid).Tests
Written test-first (red → green). Added to
tests/test_bearer_middleware.py:test_non_ascii_bearer_token_rejected_with_401— withdci.api_tokensset, anon-ASCII token must 401. Errored with
TypeErrorbefore the fix (the exactreproduction); passes after.
test_non_ascii_bearer_token_rejected_even_with_empty_list— with the empty-listopt-out (
dci.api_tokens_required=false), a non-ASCII token must still 401 ratherthan be accepted. Failed before the fix (it was returned as valid); passes after.
Existing
test_configured_tokens_*andtest_token_comparison_is_constant_timeremain green (ASCII happy path and per-candidate compare count unchanged).
./spp t spp_dci_server→ 0 failed, 0 error(s) of 334 tests. Ruff/ruff-format clean.Scope
compare_digestappears exactly once in the module. The siblingverify_dci_signaturealready wraps its body intry/except → 401, so it isnot affected by this class of crash.
19.0.2.0.3 → 19.0.2.0.4+ HISTORY fragment.README.rst/static/description/index.htmlto be regenerated from CI's pinned generator.Merge order (vs #303 DCI envelope search)
Per the cross-PR interaction analysis (2026-07-24,
internal/plans/security-prs-interaction-analysis.md): #303 and this PR both bumpspp_dci_serverto the identical19.0.2.0.4— the manifest line auto-merges with no conflict marker. Either merge order works (code is fully disjoint: middleware vs router, and the middleware runs before any router), but whichever merges second must deliberately re-bump to19.0.2.0.5and split the HISTORY heading; the metadata files (HISTORY/README/index.html) will conflict and remind, the manifest will not.