Skip to content

fix(openai-frontend): use hmac.compare_digest for restriction header value - #8788

Open
ibondarenko1 wants to merge 1 commit into
triton-inference-server:mainfrom
ibondarenko1:hardening/constant-time-header-compare
Open

fix(openai-frontend): use hmac.compare_digest for restriction header value#8788
ibondarenko1 wants to merge 1 commit into
triton-inference-server:mainfrom
ibondarenko1:hardening/constant-time-header-compare

Conversation

@ibondarenko1

Copy link
Copy Markdown

Problem

APIRestrictionMiddleware._check_authentication at python/openai/openai_frontend/frontend/fastapi/middleware/api_restriction.py:223 validates the incoming header value with:

if not actual_value or actual_value != expected_value:

The != on two str values runs in time proportional to the matching-prefix length. A caller who can submit many requests against a restricted endpoint can, in principle, recover the configured header value byte-by-byte by observing response time. The operator-chosen header value is the only secret material guarding the endpoint, so a leak on this compare reduces the effective key strength of the restriction.

Change

Switch the compare to hmac.compare_digest, the Python standard library's constant-time string equality function. One import hmac at the top of the file, one line at the compare site, and an inline comment explaining the reason so a future contributor does not regress to == / !=.

if not actual_value or not hmac.compare_digest(actual_value, expected_value):

The short-circuit on not actual_value keeps None and empty-string headers out of compare_digest, which would raise TypeError on None. The length of actual_value is still potentially leaked (compare_digest warns about this), but length is not the secret part of an operator-configured header value; the byte content is.

Impact

  • Constant-time compare removes the byte-by-byte timing oracle on the restriction header value.
  • Verbal behavior of the middleware is unchanged: same rejection on mismatch, same 401 response body, same accept on match.
  • No public API surface change.

Backwards compatibility

  • Public API unchanged.
  • RestrictedFeatures, APIRestrictionMiddleware.__init__, and APIRestrictionMiddleware.dispatch signatures unchanged.
  • Existing config (--openai-restricted-api) parses and behaves identically.
  • The test in python/openai/tests/test_openai_restricted_apis.py exercises the pass / fail semantics and does not assert on timing; behavior under that suite is unchanged.

Tests

  • Python syntax check (python3 -c "import ast; ast.parse(open(path).read())"): pass.
  • git apply --check against main HEAD 1e69d88b0fccd77c5782c316f6f32a7c2175622f: clean.
  • Full python/openai/tests/test_openai_restricted_apis.py (467 lines, 12+ test cases) not run locally: this suite spins up a Triton server with the OpenAI frontend and a loaded engine, which needs a GPU host and a built tritonserver. The change is one-line semantics-preserving; the suite should remain green on CI.

I would happily extend the test file with one fixture that asserts on hmac.compare_digest being the compare path (e.g., mocking the compare to count call sites) if the reviewer wants the regression covered explicitly.

Why this is a hardening contribution

I was reading the OpenAI frontend's request pipeline and noticed the compare site. The same hardening pattern (constant-time compare on operator-configured header values) appears in many similar HTTP middleware projects; it is a standard idiom rather than a novel claim. The change converts a theoretical timing oracle into a no-oracle implementation at minimal review cost.

Related code paths (for reviewer context)

The C++ HTTP server (src/http_server.cc HTTPAPIServer::RespondIfRestricted) and the C++ gRPC server (src/grpc/grpc_server.cc CommonCallData::ExecutePrecondition) implement the same restriction check in their respective protocols and use the same non-constant-time compare pattern. I am happy to follow up with parallel hardening PRs against those sinks once this Python change is reviewed; keeping the changes one-per-PR per CONTRIBUTING.md's "single concern" guidance.

…value

APIRestrictionMiddleware._check_authentication compares the incoming
header value against the operator-configured expected value with !=,
which short-circuits at the first byte mismatch. A caller that can issue
many requests against a restricted endpoint can in principle infer the
configured header value byte-by-byte via response-time differences.
Switch the compare to hmac.compare_digest, the standard Python idiom
for constant-time string equality.

Signed-off-by: Ievgen Bondarenko <sactransport2000@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants