1
0
Fork 0

Merge branch v2.11 into v3.4

This commit is contained in:
kevinpollet 2025-05-23 16:16:18 +02:00
commit be0b54bade
No known key found for this signature in database
GPG key ID: 0C9A5DDD1B292453
11 changed files with 572 additions and 28 deletions

View file

@ -677,3 +677,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 |

View file

@ -5,7 +5,7 @@ description: "Enforce strict ContentLength validation in Traefik by streaming
Traefik acts as a streaming proxy. By default, it checks each chunk of data against the `Content-Length` header as it passes it on to the backend or client. This live check blocks truncated or overlong streams without holding the entire message.
If you need Traefik to read and verify the full body before any data moves on, add the [buffering middleware](../../reference/routing-configuration/http/middlewares/buffering.md):
If you need Traefik to read and verify the full body before any data moves on, add the [buffering middleware](../middlewares/http/buffering.md):
```yaml
http:

2
go.mod
View file

@ -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
)

4
go.sum
View file

@ -253,8 +253,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-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=

View file

@ -1641,6 +1641,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",

View file

@ -0,0 +1,18 @@
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: traefik.tchouk
http:
paths:
- path: /bar
backend:
resource:
kind: Service
name: service1
pathType: Prefix

View file

@ -0,0 +1,15 @@
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: traefik.tchouk
http:
paths:
- path: /bar
backend: {}
pathType: Prefix

View file

@ -315,6 +315,17 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
}
for _, pa := range rule.HTTP.Paths {
if pa.Backend.Resource != nil {
// https://kubernetes.io/docs/concepts/services-networking/ingress/#resource-backend
logger.Error().Msg("Resource backends are not supported")
continue
}
if pa.Backend.Service == nil {
logger.Error().Msg("Missing service definition")
continue
}
service, err := p.loadService(client, ingress.Namespace, pa.Backend)
if err != nil {
logger.Error().
@ -492,15 +503,6 @@ func (p *Provider) shouldProcessIngress(ingress *netv1.Ingress, ingressClasses [
}
func (p *Provider) loadService(client Client, namespace string, backend netv1.IngressBackend) (*dynamic.Service, error) {
if backend.Resource != nil {
// https://kubernetes.io/docs/concepts/services-networking/ingress/#resource-backend
return nil, errors.New("resource backends are not supported")
}
if backend.Service == nil {
return nil, errors.New("missing service definition")
}
service, exists, err := client.GetService(namespace, backend.Service.Name)
if err != nil {
return nil, err

View file

@ -522,6 +522,28 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
},
},
{
desc: "Ingress with backend resource",
allowEmptyServices: true,
expected: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
},
},
},
{
desc: "Ingress without backend",
allowEmptyServices: true,
expected: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
},
},
},
{
desc: "Ingress with one service without endpoint",
expected: &dynamic.Configuration{

View file

@ -17,6 +17,7 @@ import (
"github.com/containous/alice"
gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/gorilla/mux"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -610,20 +611,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)
}
handler = contenttype.DisableAutoDetection(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)
@ -635,6 +622,24 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
})
}
handler = contenttype.DisableAutoDetection(handler)
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{
@ -763,7 +768,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) {
@ -779,3 +784,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(),
),
))
})
}

View file

@ -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/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
"github.com/traefik/traefik/v3/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"))
})
}
}