diff --git a/spp_dci_server/README.rst b/spp_dci_server/README.rst index bc1b589d5..3423e95f4 100644 --- a/spp_dci_server/README.rst +++ b/spp_dci_server/README.rst @@ -159,6 +159,16 @@ Dependencies Changelog ========= +19.0.2.0.4 +~~~~~~~~~~ + +- fix(security): reject non-ASCII Bearer tokens with a 401 instead of + raising an unhandled error. A Bearer token carrying non-ASCII header + bytes reached ``hmac.compare_digest`` as a non-ASCII string, which + raises ``TypeError`` and surfaced as a generic 500 (with a stack + trace) on public DCI endpoints. Such tokens are now rejected before + the constant-time comparison. + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_dci_server/__manifest__.py b/spp_dci_server/__manifest__.py index ebfd938c2..e07533adc 100644 --- a/spp_dci_server/__manifest__.py +++ b/spp_dci_server/__manifest__.py @@ -1,7 +1,7 @@ { # pylint: disable=pointless-statement "name": "OpenSPP DCI Server", "summary": "DCI API server infrastructure with FastAPI routers", - "version": "19.0.2.0.3", + "version": "19.0.2.0.4", "category": "OpenSPP/Integration", "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_dci_server/middleware/signature.py b/spp_dci_server/middleware/signature.py index 82032fefe..dac3e476f 100644 --- a/spp_dci_server/middleware/signature.py +++ b/spp_dci_server/middleware/signature.py @@ -350,6 +350,23 @@ async def verify_bearer_token( headers={"WWW-Authenticate": "Bearer"}, ) + # Reject non-ASCII credentials before any comparison. HTTP headers are + # decoded as latin-1, so a non-ASCII byte reaches us as a non-ASCII str; + # hmac.compare_digest raises TypeError on non-ASCII str operands, and that + # would escape this dependency as an unhandled 500. No legitimate bearer + # credential is ever non-ASCII (OAuth2 JWTs are base64url; configured + # static tokens are ASCII), so a non-ASCII token is simply invalid. This + # guard sits before the constant-time loop, the OAuth2 path, and the + # opt-out return so every branch is covered by one check. + if not token.isascii(): + _logger.warning("DCI request has non-ASCII Bearer token") + raise DCIHTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + error_message="Invalid Bearer token", + error_code="err.auth.invalid_token", + headers={"WWW-Authenticate": "Bearer"}, + ) + # Get accepted tokens from config (comma-separated list). An empty # config used to mean "accept any non-empty token" - a fail-open # default that exposed every bearer-authenticated route the moment diff --git a/spp_dci_server/readme/HISTORY.md b/spp_dci_server/readme/HISTORY.md index 4aaf9afef..9b7737c8b 100644 --- a/spp_dci_server/readme/HISTORY.md +++ b/spp_dci_server/readme/HISTORY.md @@ -1,3 +1,11 @@ +### 19.0.2.0.4 + +- fix(security): reject non-ASCII Bearer tokens with a 401 instead of raising an + unhandled error. A Bearer token carrying non-ASCII header bytes reached + ``hmac.compare_digest`` as a non-ASCII string, which raises ``TypeError`` and + surfaced as a generic 500 (with a stack trace) on public DCI endpoints. Such + tokens are now rejected before the constant-time comparison. + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_dci_server/static/description/index.html b/spp_dci_server/static/description/index.html index 980e54294..2050f39f8 100644 --- a/spp_dci_server/static/description/index.html +++ b/spp_dci_server/static/description/index.html @@ -534,6 +534,17 @@