Use routing path in v3 matchers
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
de1802d849
commit
859f4e8868
9 changed files with 338 additions and 209 deletions
|
@ -17,7 +17,6 @@ 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"
|
||||
|
@ -630,8 +629,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
|||
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 {
|
||||
|
@ -866,76 +863,3 @@ func normalizePath(h http.Handler) http.Handler {
|
|||
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(),
|
||||
),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
ptypes "github.com/traefik/paerser/types"
|
||||
|
@ -510,56 +509,7 @@ func TestNormalizePath_malformedPercentEncoding(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// TestPathOperations tests the whole behavior of normalizePath, and sanitizePath 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.
|
||||
|
@ -580,7 +530,6 @@ func TestPathOperations(t *testing.T) {
|
|||
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)
|
||||
}))
|
||||
|
||||
|
@ -596,100 +545,88 @@ func TestPathOperations(t *testing.T) {
|
|||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
rawPath string
|
||||
expectedPath string
|
||||
expectedRaw string
|
||||
expectedRoutingPath string
|
||||
expectedStatus int
|
||||
desc string
|
||||
rawPath string
|
||||
expectedPath string
|
||||
expectedRaw 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: "normalize and sanitize path",
|
||||
rawPath: "/a/../b/%41%42%43//%2f/",
|
||||
expectedPath: "/b/ABC///",
|
||||
expectedRaw: "/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 traversal attempt",
|
||||
rawPath: "/../../b/",
|
||||
expectedPath: "/b/",
|
||||
expectedRaw: "/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 multiple traversal attempts",
|
||||
rawPath: "/a/../../b/../c/",
|
||||
expectedPath: "/c/",
|
||||
expectedRaw: "/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 mixed traversal and valid segments",
|
||||
rawPath: "/a/../b/./c/../d/",
|
||||
expectedPath: "/b/d/",
|
||||
expectedRaw: "/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 trailing slash and traversal",
|
||||
rawPath: "/a/b/../",
|
||||
expectedPath: "/a/",
|
||||
expectedRaw: "/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 encoded traversal sequences",
|
||||
rawPath: "/a/%2E%2E/%2E%2E/b/",
|
||||
expectedPath: "/b/",
|
||||
expectedRaw: "/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: "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/",
|
||||
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 unallowed percent-encoded character",
|
||||
rawPath: "/foo%20bar",
|
||||
expectedPath: "/foo bar",
|
||||
expectedRaw: "/foo%20bar",
|
||||
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 reserved percent-encoded character",
|
||||
rawPath: "/foo%2Fbar",
|
||||
expectedPath: "/foo/bar",
|
||||
expectedRaw: "/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: "routing path with unallowed and reserved percent-encoded character",
|
||||
rawPath: "/foo%20%2Fbar",
|
||||
expectedPath: "/foo /bar",
|
||||
expectedRaw: "/foo%20%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,
|
||||
desc: "path with traversal and encoded slash",
|
||||
rawPath: "/a/..%2Fb/",
|
||||
expectedPath: "/a/../b/",
|
||||
expectedRaw: "/a/..%2Fb/",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -706,7 +643,6 @@ func TestPathOperations(t *testing.T) {
|
|||
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"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue