CVE-2026-100351
ADVISORY - githubSummary
Summary
openapi3filter.ValidationHandler.Middleware validates requests with kin-openapi's
legacy router, which selects an OpenAPI operation from the decoded URL.Path. The
method is documented as implementing gorilla/mux.MiddlewareFunc, but a downstream
gorilla/mux router configured with UseEncodedPath() selects its handler from the
encoded representation (URL.RawPath).
An unauthenticated request such as /public%2Fstatus can therefore be validated as an
intentionally public operation (/public/status) and then dispatched to a protected
operation (/{id}). The configured, rejecting AuthenticationFunc is never invoked,
and the protected downstream handler runs.
This is distinct from GHSA-r277-6w6q-xmqw. The behavior below uses an explicit authentication callback that rejects missing credentials and remains reproducible after the v0.144.0 fix for the unsafe default callback.
Affected products
- Product:
github.com/getkin/kin-openapi - Component:
openapi3filter.ValidationHandler.Middleware - Confirmed post-GHSA affected releases: v0.144.0, v0.145.0, and v0.146.0
- Reporter attestation: each listed release was checked out separately, the attached
single-file test was copied into
openapi3filter/, and that test passed; maintainers should independently confirm the version range - Confirmed affected development revision:
d33e44e67734dcde81b576582185e1343fb3985d - v0.143.0 also reproduces this mechanism, but GHSA-r277-6w6q-xmqw already provides a simpler bypass on that release; the distinct post-fix claim starts at v0.144.0
- Earlier releases: not tested; please treat the introduction boundary as unknown
Security impact
An unauthenticated remote requester can reach a handler whose OpenAPI operation requires authentication when all of the following are true:
- the application uses
ValidationHandler.Middlewareas the security gate; - the downstream router preserves and interprets an encoded path, such as
gorilla/mux with
UseEncodedPath(); - a public exact route and a protected parameterized route overlap after decoding;
- the public and protected operations expose the same HTTP method;
- the protected handler relies on the validation layer's security decision.
The protected operation's path-parameter value is constrained: it is an encoded
representation that decodes to a public literal path (public%2Fstatus in this PoC),
not an arbitrary attacker-selected identifier. A protected operation without a
compatible parameterized path is not reachable through this specific mechanism.
- Primitive: the security decision is made for a different OpenAPI operation than the operation dispatched by the application router.
- Demonstrated impact: a protected handler executes while the explicitly rejecting
AuthenticationFuncis never invoked. - Impact ceiling: confidentiality or integrity loss when the reachable protected handler has that authority; the PoC does not demonstrate data disclosure or state modification.
- Availability impact: not demonstrated.
Root cause
At the confirmed development revision:
openapi3filter/validation_handler.go:ValidationHandler.Loadconstructsrouters/legacy.NewRouterunconditionally.routers/legacy/router.go: route selection uses the decoded request path.routers/gorillamux/router.go: the kin-openapi gorilla router constructs a gorilla/mux router withUseEncodedPath().ValidationHandler.Middlewareforwards the unchanged request after validation.
The component making the authentication decision and the component dispatching the handler can therefore disagree about the selected operation.
Reproduction
The attached PoC uses a real net/http server, an explicit API-key callback that
rejects missing credentials, and a downstream gorilla/mux router with
UseEncodedPath().
From a kin-openapi checkout, copy the self-contained attachment
encoded_path_route_confusion_test.go.txt into openapi3filter/ as
encoded_path_route_confusion_test.go, then run:
go test -v ./openapi3filter -run '^TestEncodedPathAuthorizationBypass$' -count=1
Expected attack observation:
status=202 downstream_route=/{id} protected_id=public%2Fstatus authentication_calls=0
wiring=Router.Use status=202 downstream_route=/{id} protected_id=public%2Fstatus authentication_calls=0
Controls included in the test show that:
/ordinaryselects the protected operation, invokes authentication once, and is rejected with HTTP 401;/public/statusreaches only the intentionally public handler;- the encoded request reaches the protected
/{id}handler with HTTP 202 while the authentication callback remains uncalled; - both external wrapping and the documented
router.Use(validationHandler.Middleware)wiring reproduce the same result.
Reverse-proxy reachability
The same request was reproduced through Nginx 1.31.3 using the official
nginx:alpine image digest
sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752.
The exact container invocation was:
docker run --rm --name kin-openapi-nginx \
--add-host=host.docker.internal:host-gateway \
-p 18081:8080 -p 18082:8081 \
-v "$PWD/poc/nginx.conf:/etc/nginx/nginx.conf:ro" \
nginx:alpine
Container port 8080 uses proxy_pass http://host.docker.internal:18080; without a
replacement URI. Host port 18081 therefore preserved /public%2Fstatus; the protected
handler returned HTTP 202 and the
authentication callback remained at zero calls. With
proxy_pass http://host.docker.internal:18080/; on container port 8081 / host port
18082, Nginx canonicalized the upstream URI; both layers selected /public/status and
the bypass did not occur.
Nginx access-log observations:
server_port=8080 "GET /public%2Fstatus HTTP/1.1" 202 0 "curl/8.7.1"
server_port=8081 "GET /public%2Fstatus HTTP/1.1" 204 0 "curl/8.7.1"
server_port=8080 "GET /ordinary HTTP/1.1" 401 0 "curl/8.7.1"
The response route headers were X-Poc-Route: /{id} on the preserving listener and
X-Poc-Route: /public/status on the canonicalizing listener. Backend observations for
the two requests were:
request_uri="/public%2Fstatus" path="/public/status" raw_path="/public%2Fstatus" auth_before=0
completed request_uri="/public%2Fstatus" auth_after=0
request_uri="/public/status" path="/public/status" raw_path="" auth_before=0
completed request_uri="/public/status" auth_after=0
This control shows that upstream path handling is a deployment precondition rather than claiming that every proxy configuration is affected.
The canonical protected control produced:
request_uri="/ordinary" path="/ordinary" raw_path="" auth_before=0
completed request_uri="/ordinary" auth_after=1
Suggested remediation
The authentication layer and downstream dispatcher must select the same OpenAPI operation from the same request representation.
Potential approaches:
- make
ValidationHandleraccept a caller-suppliedrouters.Routerand require it to match downstream routing semantics; - document that
NewValidatoralready accepts a router but is safe only when the supplied router selects operations using the same semantics as the dispatcher; callers must also supply anAuthenticationFuncthroughValidationOptions, or protected requests fail with the existing missing-authentication-service error; - as an interim hardening option, make
ValidationHandlerfail closed on an encoded path separator whenURL.PathandURL.RawPathcan select different operations, and verify this behavior against the full suite; - document the route-identity requirement and add encoded-separator regression tests.
Replacing the legacy router with the kin-openapi gorilla router closes this PoC, but a local compatibility check changed two existing missing-path-parameter tests from HTTP 400 semantics to route-not-found HTTP 404 semantics. A direct router swap is therefore not a fully validated drop-in fix.
Public-reference triage
The public advisory list and related path-encoding reports reviewed on 2026-08-18 did not describe this authentication-boundary crossing. Relevant nearby reports include issues #397, #1089, and #1105 and PR #400. This search is not proof of novelty; please deduplicate against private reports.
Credit
Preferred credit: Satoru Kanno (GMO CONNECT Inc., GMO Internet Group, Inc.)
NIST
CVSS SCORE
7.4highGitHub
CVSS SCORE
7.4highDebian
-
CVSS SCORE
N/AlowRed Hat
2.2