OpenAPI proxy exact-path allowlist + inspector body secret masking (2026-06-07)¶
Two independent defence-in-depth fixes found during an E2E security audit.
Motivation¶
1. OpenAPI Try-It proxy: exact-path entries were prefix-matched¶
api/routes/aks/openapi_proxy.py auto-injects the admin X-ELB-API-Token
on every proxied call, so the _enforce_openapi_proxy_target_path allowlist
is the gate that stops a tenant member riding that token into a non-public
upstream route. The allowlist documents two entry kinds: an entry ending in
/ is a prefix (/v1/, /docs/), and an entry without / is an
exact path (/healthz, /openapi.json).
The matcher applied lowered.startswith(prefix) to every entry, so the
exact-path entries were silently treated as prefixes: /healthzXXX,
/healthz/secret, /openapi.jsonXXX, /openapi.json/dump all passed the
allowlist and were forwarded upstream with the admin token. The deny-list
still blocked /admin, /internal, /debug, /private, /sudo, but any
other upstream route sharing a /healthz or /openapi.json prefix became
reachable — a violation of the documented exact-path contract and an
unnecessary widening of the token-bearing surface.
2. Request inspector: captured bodies were not secret-masked¶
api/services/request_metrics.py::capture_body decoded request/response
bodies for the SPA's request-detail inspector feed but, unlike
redact_headers, applied no secret masking to the body. With full body
capture enabled (REQUEST_DETAIL_CAPTURE_ENABLED=true, an operator debug
action), a captured body could carry a bearer token, a SAS signature, an
account/access key, a client secret, a connection string, or a password
(e.g. the OpenAPI proxy whose upstream may echo the injected admin token in
an error) verbatim into the inspector UI — contrary to charter §12
("never echo tokens … or full SAS URLs").
User-facing change¶
No change to legitimate flows. The SPA's API Reference Try-It calls
(/healthz, /openapi.json, /v1/*, /docs/*) keep working; only
prefix-abuse variants of the exact entries are now rejected with the
existing openapi_path_not_allowlisted 400. Inspector bodies now show
Bearer <redacted> / ?<sas-redacted> etc. in place of real secrets.
API / IaC diff summary¶
api/routes/aks/openapi_proxy.py—_enforce_openapi_proxy_target_path: the final allowlist loop now branches onprefix.endswith("/"). Prefix entries allow the bare root or anything beneath; exact entries (/healthz,/openapi.json) require an exact match. Deny-list and traversal/control-char guards are unchanged and still run first.api/services/request_metrics.py—capture_bodyappliessanitise(text, mask_subscription_ids=False)after decode. Subscription/ tenant GUIDs are intentionally preserved (the dashboard surfaces the caller's own subscription throughout, and masking them would destroy the inspector's debug utility); only genuinely sensitive token/key/SAS/secret shapes are masked. Body capture remains default-OFF.- No IaC change. No RBAC change (charter §12a: no role narrowed; persona
matrix unaffected; no
require_calleradded to any SSE stream).
Validation evidence¶
uv run pytest -q api/tests/test_openapi_proxy_route.py— 31 passed, including the new parametrizedtest_openapi_proxy_exact_entries_are_not_prefix_matched(/healthzXXX,/healthz/secret,/healthz-internal,/openapi.jsonXXX,/openapi.json/dumpall rejected) while/healthz,/openapi.json,/v1/*,/docs/*still allowed.uv run pytest -q api/tests/test_request_metrics_detail.py— 12 passed, including the newtest_capture_body_masks_secrets_but_keeps_subscription_ids.- Full
uv run pytest -q api/tests+ persona matrix green;ruffclean. - Live: the OpenAPI proxy allowlist + traversal guards were exercised on the
running cluster (
/openapi.json200;/,/status,/secret/keys,/../adminall 400) during the same audit.