Traefik allows path traversal using url encoding
Description
Traefik (pronounced traffic) is an HTTP reverse proxy and load balancer. Prior to versions 2.11.25 and 3.4.1, there is a potential vulnerability in Traefik managing the requests using a PathPrefix, Path or PathRegex matcher. When Traefik is configured to route the requests to a backend using a matcher based on the path, if the URL contains a URL encoded string in its path, it’s possible to target a backend, exposed using another router, by-passing the middlewares chain. This issue has been patched in versions 2.11.25 and 3.4.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Traefik before 2.11.25 and 3.4.1 allows bypassing middleware chains by using URL-encoded path traversal sequences (e.g., %2e%2e) in path-based routing rules.
Vulnerability
Description
Traefik versions prior to 2.11.25 and 3.4.1 are vulnerable to a path traversal bypass in the URL routing logic. The bug affects routers using PathPrefix, Path, or PathRegex matchers. When a request contains URL-encoded strings such as %2e%2e (representing ..), the path normalization step could be bypassed, allowing an attacker to craft a URL that routes to a different backend than intended, while skipping any middlewares that would normally apply [1][4].
Exploitation
An attacker can exploit this by sending a specially crafted HTTP request to a Traefik instance with path-based routing rules. For example, if a router matches PathPrefix('/service') and applies a middleware, and another router matches PathPrefix('/service/sub-path') without that middleware, a request to /service/sub-path/%2e%2e/other-path could be routed to the first router’s backend, bypassing the middleware chain [4]. The vulnerability is triggered by URL-encoded path traversal sequences that are not sanitized before routing decisions are made.
Impact
Successful exploitation allows an attacker to bypass middleware security controls—such as authentication, rate limiting, or header modification—and access backend services that should be protected. This can lead to unauthorized access to sensitive resources or endpoints that are intended to be restricted [1][4].
Mitigation
The vulnerability has been patched in Traefik versions 2.11.25 and 3.4.1. Users should upgrade immediately. The fix normalizes the request path after routing, ensuring that URL-encoded traversal sequences are resolved before the route is selected [2]. There is no known workaround other than upgrading.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/traefik/traefik/v3Go | < 3.4.1 | 3.4.1 |
github.com/traefik/traefik/v2Go | < 2.11.25 | 2.11.25 |
github.com/traefik/traefikGo | <= 1.7.34 | — |
Affected products
8- ghsa-coords6 versionspkg:golang/github.com/traefik/traefikpkg:golang/github.com/traefik/traefik/v2pkg:golang/github.com/traefik/traefik/v3pkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/traefik2&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/traefik&distro=openSUSE%20Tumbleweed
<= 1.7.34+ 5 more
- (no CPE)range: <= 1.7.34
- (no CPE)range: < 2.11.25
- (no CPE)range: < 3.4.1
- (no CPE)range: < 0.0.20250529T205903-1.1
- (no CPE)range: < 2.11.26-1.1
- (no CPE)range: < 3.4.3-1.1
Patches
108d5dfee0164Normalize request path
7 files changed · +504 −17
docs/content/migration/v2.md+29 −0 modified@@ -674,3 +674,32 @@ it can lead to unsafe routing when the `sanitizePath` option is set to `false`. Setting the `sanitizePath` option to `false` is not safe. Ensure every request is properly url encoded instead. + +## v2.11.25 + +### Request Path Normalization + +Since `v2.11.25`, the request path is now normalized by decoding unreserved characters in the request path, +and also uppercasing the percent-encoded characters. +This follows [RFC 3986 percent-encoding normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.2), +and [RFC 3986 case normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1). + +The normalization happens before the request path is sanitized, +and cannot be disabled. +This notably helps with encoded dots characters (which are unreserved characters) to be sanitized properly. + +### Routing Path + +Since `v2.11.25`, the reserved characters [(as per RFC 3986)](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) are kept encoded in the request path when matching the router rules. +Those characters, when decoded, change the meaning of the request path for routing purposes, +and Traefik now keeps them encoded to avoid any ambiguity. + +### Request Path Matching Examples + +| Request Path | Router Rule | Traefik v2.11.24 | Traefik v2.11.25 | +|-------------------|------------------------|------------------|------------------| +| `/foo%2Fbar` | PathPrefix(`/foo/bar`) | Match | No match | +| `/foo/../bar` | PathPrefix(`/foo`) | No match | No match | +| `/foo/../bar` | PathPrefix(`/bar`) | Match | Match | +| `/foo/%2E%2E/bar` | PathPrefix(`/foo`) | Match | No match | +| `/foo/%2E%2E/bar` | PathPrefix(`/bar`) | No match | Match |
go.mod+1 −1 modified@@ -391,7 +391,7 @@ require ( // Containous forks replace ( github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e - github.com/gorilla/mux => github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f + github.com/gorilla/mux => github.com/containous/mux v0.0.0-20250523120546-41b6ec3aed59 github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 )
go.sum+2 −2 modified@@ -298,8 +298,8 @@ github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e h1:D+uTE github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e/go.mod h1:s8kLgBQolDbsJOPVIGCEEv9zGAKUUf/685Gi0Qqg8z8= github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 h1:aPspFRO6b94To3gl4yTDOEtpjFwXI7V2W+z0JcNljQ4= github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595/go.mod h1:+lHFbEasIiQVGzhVDVw/cn0ZaOzde2OwNncp1NhXV4c= -github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f h1:1uEtynq2C0ljy3630jt7EAxg8jZY2gy6YHdGwdqEpWw= -github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= +github.com/containous/mux v0.0.0-20250523120546-41b6ec3aed59 h1:lJUOWjGohYjLKEfAz2nyI/dpzfKNPQLi5GLH7aaOZkw= +github.com/containous/mux v0.0.0-20250523120546-41b6ec3aed59/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
integration/simple_test.go+12 −0 modified@@ -1429,6 +1429,18 @@ func (s *SimpleSuite) TestSanitizePath() { target: "127.0.0.1:8000", expected: http.StatusFound, }, + { + desc: "Implicit encoded dot dots call to the route with a middleware", + request: "GET /without/%2E%2E/with HTTP/1.1\r\nHost: other.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusFound, + }, + { + desc: "Implicit with encoded unreserved character call to the route with a middleware", + request: "GET /%77ith HTTP/1.1\r\nHost: other.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusFound, + }, { desc: "Explicit call to the route with a middleware, and disable path sanitization", request: "GET /with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
pkg/muxer/http/mux.go+1 −1 modified@@ -48,7 +48,7 @@ func NewMuxer() (*Muxer, error) { } return &Muxer{ - Router: mux.NewRouter().SkipClean(true), + Router: mux.NewRouter().UseRoutingPath().SkipClean(true), parser: parser, }, nil }
pkg/server/server_entrypoint_tcp.go+173 −13 modified@@ -16,6 +16,7 @@ import ( "time" "github.com/containous/alice" + "github.com/gorilla/mux" "github.com/pires/go-proxyproto" "github.com/sirupsen/logrus" "github.com/traefik/traefik/v2/pkg/config/static" @@ -571,18 +572,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati return nil, err } - if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath { - // sanitizePath is used to clean the URL path by removing /../, /./ and duplicate slash sequences, - // to make sure the path is interpreted by the backends as it is evaluated inside rule matchers. - handler = sanitizePath(handler) - } - - if configuration.HTTP.EncodeQuerySemicolons { - handler = encodeQuerySemicolons(handler) - } else { - handler = http.AllowQuerySemicolons(handler) - } - debugConnection := os.Getenv(debugConnectionEnv) != "" if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { handler = newKeepAliveMiddleware(handler, configuration.Transport.KeepAliveMaxRequests, configuration.Transport.KeepAliveMaxTime) @@ -594,6 +583,22 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati }) } + if configuration.HTTP.EncodeQuerySemicolons { + handler = encodeQuerySemicolons(handler) + } else { + handler = http.AllowQuerySemicolons(handler) + } + + handler = routingPath(handler) + + // Note that the Path sanitization has to be done after the path normalization, + // hence the wrapping has to be done before the normalize path wrapping. + if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath { + handler = sanitizePath(handler) + } + + handler = normalizePath(handler) + handler = denyFragment(handler) serverHTTP := &http.Server{ @@ -721,7 +726,7 @@ func denyFragment(h http.Handler) http.Handler { }) } -// sanitizePath removes the "..", "." and duplicate slash segments from the URL. +// sanitizePath removes the "..", "." and duplicate slash segments from the URL according to https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3. // It cleans the request URL Path and RawPath, and updates the request URI. func sanitizePath(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { @@ -737,3 +742,158 @@ func sanitizePath(h http.Handler) http.Handler { h.ServeHTTP(rw, r2) }) } + +// unreservedCharacters contains the mapping of the percent-encoded form to the ASCII form +// of the unreserved characters according to https://datatracker.ietf.org/doc/html/rfc3986#section-2.3. +var unreservedCharacters = map[string]rune{ + "%41": 'A', "%42": 'B', "%43": 'C', "%44": 'D', "%45": 'E', "%46": 'F', + "%47": 'G', "%48": 'H', "%49": 'I', "%4A": 'J', "%4B": 'K', "%4C": 'L', + "%4D": 'M', "%4E": 'N', "%4F": 'O', "%50": 'P', "%51": 'Q', "%52": 'R', + "%53": 'S', "%54": 'T', "%55": 'U', "%56": 'V', "%57": 'W', "%58": 'X', + "%59": 'Y', "%5A": 'Z', + + "%61": 'a', "%62": 'b', "%63": 'c', "%64": 'd', "%65": 'e', "%66": 'f', + "%67": 'g', "%68": 'h', "%69": 'i', "%6A": 'j', "%6B": 'k', "%6C": 'l', + "%6D": 'm', "%6E": 'n', "%6F": 'o', "%70": 'p', "%71": 'q', "%72": 'r', + "%73": 's', "%74": 't', "%75": 'u', "%76": 'v', "%77": 'w', "%78": 'x', + "%79": 'y', "%7A": 'z', + + "%30": '0', "%31": '1', "%32": '2', "%33": '3', "%34": '4', + "%35": '5', "%36": '6', "%37": '7', "%38": '8', "%39": '9', + + "%2D": '-', "%2E": '.', "%5F": '_', "%7E": '~', +} + +// normalizePath removes from the RawPath unreserved percent-encoded characters as they are equivalent to their non-encoded +// form according to https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 and capitalizes percent-encoded characters +// according to https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1. +func normalizePath(h http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rawPath := req.URL.RawPath + + // When the RawPath is empty the encoded form of the Path is equivalent to the original request Path. + // Thus, the normalization is not needed as no unreserved characters were encoded and the encoded version + // of Path obtained with URL.EscapedPath contains only percent-encoded characters in upper case. + if rawPath == "" { + h.ServeHTTP(rw, req) + return + } + + var normalizedRawPathBuilder strings.Builder + for i := 0; i < len(rawPath); i++ { + if rawPath[i] != '%' { + normalizedRawPathBuilder.WriteString(string(rawPath[i])) + continue + } + + // This should never happen as the standard library will reject requests containing invalid percent-encodings. + // This discards URLs with a percent character at the end. + if i+2 >= len(rawPath) { + rw.WriteHeader(http.StatusBadRequest) + return + } + + encodedCharacter := strings.ToUpper(rawPath[i : i+3]) + if r, unreserved := unreservedCharacters[encodedCharacter]; unreserved { + normalizedRawPathBuilder.WriteRune(r) + } else { + normalizedRawPathBuilder.WriteString(encodedCharacter) + } + + i += 2 + } + + normalizedRawPath := normalizedRawPathBuilder.String() + + // We do not have to alter the request URL as the original RawPath is already normalized. + if normalizedRawPath == rawPath { + h.ServeHTTP(rw, req) + return + } + + r2 := new(http.Request) + *r2 = *req + + // Decoding unreserved characters only alter the RAW version of the URL, + // as unreserved percent-encoded characters are equivalent to their non encoded form. + r2.URL.RawPath = normalizedRawPath + + // Because the reverse proxy director is building query params from RequestURI it needs to be updated as well. + r2.RequestURI = r2.URL.RequestURI() + + h.ServeHTTP(rw, r2) + }) +} + +// reservedCharacters contains the mapping of the percent-encoded form to the ASCII form +// of the reserved characters according to https://datatracker.ietf.org/doc/html/rfc3986#section-2.2. +// By extension to https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 the percent character is also considered a reserved character. +// Because decoding the percent character would change the meaning of the URL. +var reservedCharacters = map[string]rune{ + "%3A": ':', + "%2F": '/', + "%3F": '?', + "%23": '#', + "%5B": '[', + "%5D": ']', + "%40": '@', + "%21": '!', + "%24": '$', + "%26": '&', + "%27": '\'', + "%28": '(', + "%29": ')', + "%2A": '*', + "%2B": '+', + "%2C": ',', + "%3B": ';', + "%3D": '=', + "%25": '%', +} + +// routingPath decodes non-allowed characters in the EscapedPath and stores it in the context to be able to use it for routing. +// This allows using the decoded version of the non-allowed characters in the routing rules for a better UX. +// For example, the rule PathPrefix(`/foo bar`) will match the following request path `/foo%20bar`. +func routingPath(h http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + escapedPath := req.URL.EscapedPath() + + var routingPathBuilder strings.Builder + for i := 0; i < len(escapedPath); i++ { + if escapedPath[i] != '%' { + routingPathBuilder.WriteString(string(escapedPath[i])) + continue + } + + // This should never happen as the standard library will reject requests containing invalid percent-encodings. + // This discards URLs with a percent character at the end. + if i+2 >= len(escapedPath) { + rw.WriteHeader(http.StatusBadRequest) + return + } + + encodedCharacter := escapedPath[i : i+3] + if _, reserved := reservedCharacters[encodedCharacter]; reserved { + routingPathBuilder.WriteString(encodedCharacter) + } else { + // This should never happen as the standard library will reject requests containing invalid percent-encodings. + decodedCharacter, err := url.PathUnescape(encodedCharacter) + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + routingPathBuilder.WriteString(decodedCharacter) + } + + i += 2 + } + + h.ServeHTTP(rw, req.WithContext( + context.WithValue( + req.Context(), + mux.RoutingPathKey, + routingPathBuilder.String(), + ), + )) + }) +}
pkg/server/server_entrypoint_tcp_test.go+286 −0 modified@@ -13,10 +13,12 @@ import ( "testing" "time" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/static" + "github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator" tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp" "github.com/traefik/traefik/v2/pkg/tcp" "golang.org/x/net/http2" @@ -424,3 +426,287 @@ func TestSanitizePath(t *testing.T) { }) } } + +func TestNormalizePath(t *testing.T) { + unreservedDecoded := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + unreserved := []string{ + "%41", "%42", "%43", "%44", "%45", "%46", "%47", "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F", "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", "%58", "%59", "%5A", + "%61", "%62", "%63", "%64", "%65", "%66", "%67", "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77", "%78", "%79", "%7A", + "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37", "%38", "%39", + "%2D", "%2E", "%5F", "%7E", + } + + reserved := []string{ + "%3A", "%2F", "%3F", "%23", "%5B", "%5D", "%40", "%21", "%24", "%26", "%27", "%28", "%29", "%2A", "%2B", "%2C", "%3B", "%3D", "%25", + } + reservedJoined := strings.Join(reserved, "") + + unallowedCharacter := "%0a" // line feed + unallowedCharacterUpperCased := "%0A" // line feed upper case + + var callCount int + handler := normalizePath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + + wantRawPath := "/" + unreservedDecoded + reservedJoined + unallowedCharacterUpperCased + assert.Equal(t, wantRawPath, r.URL.RawPath) + })) + + req := httptest.NewRequest(http.MethodGet, "http://foo/"+strings.Join(unreserved, "")+reservedJoined+unallowedCharacter, http.NoBody) + res := httptest.NewRecorder() + + handler.ServeHTTP(res, req) + + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, 1, callCount) +} + +func TestNormalizePath_malformedPercentEncoding(t *testing.T) { + tests := []struct { + desc string + path string + wantErr bool + }{ + { + desc: "well formed path", + path: "/%20", + }, + { + desc: "percent sign at the end", + path: "/%", + wantErr: true, + }, + { + desc: "incomplete percent encoding at the end", + path: "/%f", + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var callCount int + handler := normalizePath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + })) + + req := httptest.NewRequest(http.MethodGet, "http://foo", http.NoBody) + req.URL.RawPath = test.path + + res := httptest.NewRecorder() + + handler.ServeHTTP(res, req) + + if test.wantErr { + assert.Equal(t, http.StatusBadRequest, res.Code) + assert.Equal(t, 0, callCount) + } else { + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, 1, callCount) + } + }) + } +} + +func TestRoutingPath(t *testing.T) { + tests := []struct { + desc string + path string + expRoutingPath string + expStatus int + }{ + { + desc: "unallowed percent-encoded character is decoded", + path: "/foo%20bar", + expRoutingPath: "/foo bar", + expStatus: http.StatusOK, + }, + { + desc: "reserved percent-encoded character is kept encoded", + path: "/foo%2Fbar", + expRoutingPath: "/foo%2Fbar", + expStatus: http.StatusOK, + }, + { + desc: "multiple mixed characters", + path: "/foo%20bar%2Fbaz%23qux", + expRoutingPath: "/foo bar%2Fbaz%23qux", + expStatus: http.StatusOK, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var gotRoute string + handler := routingPath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotRoute, _ = r.Context().Value(mux.RoutingPathKey).(string) + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "http://foo"+test.path, http.NoBody) + + res := httptest.NewRecorder() + + handler.ServeHTTP(res, req) + + assert.Equal(t, test.expStatus, res.Code) + assert.Equal(t, test.expRoutingPath, gotRoute) + }) + } +} + +// TestPathOperations tests the whole behavior of normalizePath, sanitizePath, and routingPath combined through the use of the createHTTPServer func. +// It aims to guarantee the server entrypoint handler is secure regarding a large variety of cases that could lead to path traversal attacks. +func TestPathOperations(t *testing.T) { + // Create a listener for the server. + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { + _ = ln.Close() + }) + + // Define the server configuration. + configuration := &static.EntryPoint{} + configuration.SetDefaults() + + // Create the HTTP server using createHTTPServer. + server, err := createHTTPServer(context.Background(), ln, configuration, false, requestdecorator.New(nil)) + require.NoError(t, err) + + server.Switcher.UpdateHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Path", r.URL.Path) + w.Header().Set("RawPath", r.URL.EscapedPath()) + w.Header().Set("RoutingPath", r.Context().Value(mux.RoutingPathKey).(string)) + w.WriteHeader(http.StatusOK) + })) + + go func() { + // server is expected to return an error if the listener is closed. + _ = server.Server.Serve(ln) + }() + + client := http.Client{ + Transport: &http.Transport{ + ResponseHeaderTimeout: 1 * time.Second, + }, + } + + tests := []struct { + desc string + rawPath string + expectedPath string + expectedRaw string + expectedRoutingPath string + expectedStatus int + }{ + { + desc: "normalize and sanitize path", + rawPath: "/a/../b/%41%42%43//%2f/", + expectedPath: "/b/ABC///", + expectedRaw: "/b/ABC/%2F/", + expectedRoutingPath: "/b/ABC/%2F/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with traversal attempt", + rawPath: "/../../b/", + expectedPath: "/b/", + expectedRaw: "/b/", + expectedRoutingPath: "/b/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with multiple traversal attempts", + rawPath: "/a/../../b/../c/", + expectedPath: "/c/", + expectedRaw: "/c/", + expectedRoutingPath: "/c/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with mixed traversal and valid segments", + rawPath: "/a/../b/./c/../d/", + expectedPath: "/b/d/", + expectedRaw: "/b/d/", + expectedRoutingPath: "/b/d/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with trailing slash and traversal", + rawPath: "/a/b/../", + expectedPath: "/a/", + expectedRaw: "/a/", + expectedRoutingPath: "/a/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with encoded traversal sequences", + rawPath: "/a/%2E%2E/%2E%2E/b/", + expectedPath: "/b/", + expectedRaw: "/b/", + expectedRoutingPath: "/b/", + expectedStatus: http.StatusOK, + }, + { + desc: "path with over-encoded traversal sequences", + rawPath: "/a/%252E%252E/%252E%252E/b/", + expectedPath: "/a/%2E%2E/%2E%2E/b/", + expectedRaw: "/a/%252E%252E/%252E%252E/b/", + expectedRoutingPath: "/a/%252E%252E/%252E%252E/b/", + expectedStatus: http.StatusOK, + }, + { + desc: "routing path with unallowed percent-encoded character", + rawPath: "/foo%20bar", + expectedPath: "/foo bar", + expectedRaw: "/foo%20bar", + expectedRoutingPath: "/foo bar", + expectedStatus: http.StatusOK, + }, + { + desc: "routing path with reserved percent-encoded character", + rawPath: "/foo%2Fbar", + expectedPath: "/foo/bar", + expectedRaw: "/foo%2Fbar", + expectedRoutingPath: "/foo%2Fbar", + expectedStatus: http.StatusOK, + }, + { + desc: "routing path with unallowed and reserved percent-encoded character", + rawPath: "/foo%20%2Fbar", + expectedPath: "/foo /bar", + expectedRaw: "/foo%20%2Fbar", + expectedRoutingPath: "/foo %2Fbar", + expectedStatus: http.StatusOK, + }, + { + desc: "path with traversal and encoded slash", + rawPath: "/a/..%2Fb/", + expectedPath: "/a/../b/", + expectedRaw: "/a/..%2Fb/", + expectedRoutingPath: "/a/..%2Fb/", + expectedStatus: http.StatusOK, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, "http://"+ln.Addr().String()+test.rawPath, http.NoBody) + require.NoError(t, err) + + res, err := client.Do(req) + require.NoError(t, err) + + assert.Equal(t, test.expectedStatus, res.StatusCode) + assert.Equal(t, test.expectedPath, res.Header.Get("Path")) + assert.Equal(t, test.expectedRaw, res.Header.Get("RawPath")) + assert.Equal(t, test.expectedRoutingPath, res.Header.Get("RoutingPath")) + }) + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-vrch-868g-9jx5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-47952ghsaADVISORY
- github.com/traefik/traefik/commit/08d5dfee0164aa54dd44a467870042e18e8d3f00ghsax_refsource_MISCWEB
- github.com/traefik/traefik/releases/tag/v2.11.25ghsax_refsource_MISCWEB
- github.com/traefik/traefik/releases/tag/v3.4.1ghsax_refsource_MISCWEB
- github.com/traefik/traefik/security/advisories/GHSA-vrch-868g-9jx5ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.