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
|
@ -290,3 +290,32 @@ and to help with the migration from v2 to v3.
|
||||||
The `ruleSyntax` router's option was used to override the default rule syntax for a specific router.
|
The `ruleSyntax` router's option was used to override the default rule syntax for a specific router.
|
||||||
|
|
||||||
In preparation for the next major release, please remove any use of these two options and use the v3 syntax for writing the router's rules.
|
In preparation for the next major release, please remove any use of these two options and use the v3 syntax for writing the router's rules.
|
||||||
|
|
||||||
|
## v3.4.1
|
||||||
|
|
||||||
|
### Request Path Normalization
|
||||||
|
|
||||||
|
Since `v3.4.1`, 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 `v3.4.1`, 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 v3.4.0 | Traefik v3.4.1 |
|
||||||
|
|-------------------|------------------------|----------------|----------------|
|
||||||
|
| `/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 |
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
[providers.file]
|
[providers.file]
|
||||||
filename = "{{ .SelfFilename }}"
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
[core]
|
||||||
|
defaultRuleSyntax = "{{ .DefaultRuleSyntax }}"
|
||||||
|
|
||||||
# dynamic configuration
|
# dynamic configuration
|
||||||
[http.routers]
|
[http.routers]
|
||||||
[http.routers.without]
|
[http.routers.without]
|
|
@ -1606,9 +1606,108 @@ func (s *SimpleSuite) TestSanitizePath() {
|
||||||
|
|
||||||
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
|
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
|
||||||
|
|
||||||
file := s.adaptFile("fixtures/simple_clean_path.toml", struct {
|
file := s.adaptFile("fixtures/simple_sanitize_path.toml", struct {
|
||||||
Server1 string
|
Server1 string
|
||||||
}{whoami1URL})
|
DefaultRuleSyntax string
|
||||||
|
}{whoami1URL, "v3"})
|
||||||
|
|
||||||
|
s.traefikCmd(withConfigFile(file))
|
||||||
|
|
||||||
|
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix(`/with`)"))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
request string
|
||||||
|
target string
|
||||||
|
body string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Explicit call to the route with a middleware",
|
||||||
|
request: "GET /with 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 without a middleware",
|
||||||
|
request: "GET /without HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
|
||||||
|
target: "127.0.0.1:8000",
|
||||||
|
expected: http.StatusOK,
|
||||||
|
body: "GET /without HTTP/1.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Implicit call to the route with a middleware",
|
||||||
|
request: "GET /without/../with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
|
||||||
|
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",
|
||||||
|
target: "127.0.0.1:8001",
|
||||||
|
expected: http.StatusFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Explicit call to the route without a middleware, and disable path sanitization",
|
||||||
|
request: "GET /without HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
|
||||||
|
target: "127.0.0.1:8001",
|
||||||
|
expected: http.StatusOK,
|
||||||
|
body: "GET /without HTTP/1.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Implicit call to the route with a middleware, and disable path sanitization",
|
||||||
|
request: "GET /without/../with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
|
||||||
|
target: "127.0.0.1:8001",
|
||||||
|
// The whoami is redirecting to /with, but the path is not sanitized.
|
||||||
|
expected: http.StatusMovedPermanently,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
conn, err := net.Dial("tcp", test.target)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte(test.request))
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
|
||||||
|
assert.Equalf(s.T(), test.expected, resp.StatusCode, "%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
|
||||||
|
|
||||||
|
if test.body != "" {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
assert.Contains(s.T(), string(body), test.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestSanitizePathSyntaxV2() {
|
||||||
|
s.createComposeProject("base")
|
||||||
|
|
||||||
|
s.composeUp()
|
||||||
|
defer s.composeDown()
|
||||||
|
|
||||||
|
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
|
||||||
|
|
||||||
|
file := s.adaptFile("fixtures/simple_sanitize_path.toml", struct {
|
||||||
|
Server1 string
|
||||||
|
DefaultRuleSyntax string
|
||||||
|
}{whoami1URL, "v2"})
|
||||||
|
|
||||||
s.traefikCmd(withConfigFile(file))
|
s.traefikCmd(withConfigFile(file))
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,8 @@ func path(tree *matchersTree, paths ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.matcher = func(req *http.Request) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return req.URL.Path == path
|
routingPath := getRoutingPath(req)
|
||||||
|
return routingPath != nil && *routingPath == path
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -157,7 +158,8 @@ func pathRegexp(tree *matchersTree, paths ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.matcher = func(req *http.Request) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return re.MatchString(req.URL.Path)
|
routingPath := getRoutingPath(req)
|
||||||
|
return routingPath != nil && re.MatchString(*routingPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -171,7 +173,8 @@ func pathPrefix(tree *matchersTree, paths ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.matcher = func(req *http.Request) bool {
|
tree.matcher = func(req *http.Request) bool {
|
||||||
return strings.HasPrefix(req.URL.Path, path)
|
routingPath := getRoutingPath(req)
|
||||||
|
return routingPath != nil && strings.HasPrefix(*routingPath, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -28,7 +28,7 @@ func pathV2(tree *matchersTree, paths ...string) error {
|
||||||
var routes []*mux.Route
|
var routes []*mux.Route
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
route := mux.NewRouter().NewRoute()
|
route := mux.NewRouter().UseRoutingPath().NewRoute()
|
||||||
|
|
||||||
if err := route.Path(path).GetError(); err != nil {
|
if err := route.Path(path).GetError(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -54,7 +54,7 @@ func pathPrefixV2(tree *matchersTree, paths ...string) error {
|
||||||
var routes []*mux.Route
|
var routes []*mux.Route
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
route := mux.NewRouter().NewRoute()
|
route := mux.NewRouter().UseRoutingPath().NewRoute()
|
||||||
|
|
||||||
if err := route.PathPrefix(path).GetError(); err != nil {
|
if err := route.PathPrefix(path).GetError(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v3/pkg/rules"
|
"github.com/traefik/traefik/v3/pkg/rules"
|
||||||
)
|
)
|
||||||
|
@ -33,6 +38,16 @@ func NewMuxer(parser SyntaxParser) *Muxer {
|
||||||
// ServeHTTP forwards the connection to the matching HTTP handler.
|
// ServeHTTP forwards the connection to the matching HTTP handler.
|
||||||
// Serves 404 if no handler is found.
|
// Serves 404 if no handler is found.
|
||||||
func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
logger := log.Ctx(req.Context())
|
||||||
|
|
||||||
|
var err error
|
||||||
|
req, err = withRoutingPath(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug().Err(err).Msg("Unable to add routing path to request context")
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, route := range m.routes {
|
for _, route := range m.routes {
|
||||||
if route.matchers.match(req) {
|
if route.matchers.match(req) {
|
||||||
route.handler.ServeHTTP(rw, req)
|
route.handler.ServeHTTP(rw, req)
|
||||||
|
@ -72,6 +87,86 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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": '%',
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRoutingPath retrieves the routing path from the request context.
|
||||||
|
// It returns nil if the routing path is not set in the context.
|
||||||
|
func getRoutingPath(req *http.Request) *string {
|
||||||
|
routingPath := req.Context().Value(mux.RoutingPathKey)
|
||||||
|
if routingPath != nil {
|
||||||
|
rp := routingPath.(string)
|
||||||
|
return &rp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// withRoutingPath decodes non-allowed characters in the EscapedPath and stores it in the request 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 withRoutingPath(req *http.Request) (*http.Request, error) {
|
||||||
|
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) {
|
||||||
|
return nil, errors.New("invalid percent-encoding at the end of the URL path")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, errors.New("invalid percent-encoding in URL path")
|
||||||
|
}
|
||||||
|
routingPathBuilder.WriteString(decodedCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.WithContext(
|
||||||
|
context.WithValue(
|
||||||
|
req.Context(),
|
||||||
|
mux.RoutingPathKey,
|
||||||
|
routingPathBuilder.String(),
|
||||||
|
),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ParseDomains extract domains from rule.
|
// ParseDomains extract domains from rule.
|
||||||
func ParseDomains(rule string) ([]string, error) {
|
func ParseDomains(rule string) ([]string, error) {
|
||||||
var matchers []string
|
var matchers []string
|
||||||
|
|
|
@ -557,3 +557,43 @@ func TestGetRulePriority(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRoutingPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
path string
|
||||||
|
expectedRoutingPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "unallowed percent-encoded character is decoded",
|
||||||
|
path: "/foo%20bar",
|
||||||
|
expectedRoutingPath: "/foo bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "reserved percent-encoded character is kept encoded",
|
||||||
|
path: "/foo%2Fbar",
|
||||||
|
expectedRoutingPath: "/foo%2Fbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple mixed characters",
|
||||||
|
path: "/foo%20bar%2Fbaz%23qux",
|
||||||
|
expectedRoutingPath: "/foo bar%2Fbaz%23qux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://foo"+test.path, http.NoBody)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
req, err = withRoutingPath(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gotRoutingPath := getRoutingPath(req)
|
||||||
|
assert.NotNil(t, gotRoutingPath)
|
||||||
|
assert.Equal(t, test.expectedRoutingPath, *gotRoutingPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
|
|
||||||
"github.com/containous/alice"
|
"github.com/containous/alice"
|
||||||
gokitmetrics "github.com/go-kit/kit/metrics"
|
gokitmetrics "github.com/go-kit/kit/metrics"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -630,8 +629,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
||||||
handler = http.AllowQuerySemicolons(handler)
|
handler = http.AllowQuerySemicolons(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = routingPath(handler)
|
|
||||||
|
|
||||||
// Note that the Path sanitization has to be done after the path normalization,
|
// 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.
|
// hence the wrapping has to be done before the normalize path wrapping.
|
||||||
if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath {
|
if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath {
|
||||||
|
@ -866,76 +863,3 @@ func normalizePath(h http.Handler) http.Handler {
|
||||||
h.ServeHTTP(rw, r2)
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
ptypes "github.com/traefik/paerser/types"
|
ptypes "github.com/traefik/paerser/types"
|
||||||
|
@ -510,56 +509,7 @@ func TestNormalizePath_malformedPercentEncoding(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoutingPath(t *testing.T) {
|
// TestPathOperations tests the whole behavior of normalizePath, and sanitizePath combined through the use of the createHTTPServer func.
|
||||||
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.
|
// 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) {
|
func TestPathOperations(t *testing.T) {
|
||||||
// Create a listener for the server.
|
// 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) {
|
server.Switcher.UpdateHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Path", r.URL.Path)
|
w.Header().Set("Path", r.URL.Path)
|
||||||
w.Header().Set("RawPath", r.URL.EscapedPath())
|
w.Header().Set("RawPath", r.URL.EscapedPath())
|
||||||
w.Header().Set("RoutingPath", r.Context().Value(mux.RoutingPathKey).(string))
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -600,7 +549,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath string
|
rawPath string
|
||||||
expectedPath string
|
expectedPath string
|
||||||
expectedRaw string
|
expectedRaw string
|
||||||
expectedRoutingPath string
|
|
||||||
expectedStatus int
|
expectedStatus int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -608,7 +556,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/../b/%41%42%43//%2f/",
|
rawPath: "/a/../b/%41%42%43//%2f/",
|
||||||
expectedPath: "/b/ABC///",
|
expectedPath: "/b/ABC///",
|
||||||
expectedRaw: "/b/ABC/%2F/",
|
expectedRaw: "/b/ABC/%2F/",
|
||||||
expectedRoutingPath: "/b/ABC/%2F/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -616,7 +563,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/../../b/",
|
rawPath: "/../../b/",
|
||||||
expectedPath: "/b/",
|
expectedPath: "/b/",
|
||||||
expectedRaw: "/b/",
|
expectedRaw: "/b/",
|
||||||
expectedRoutingPath: "/b/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -624,7 +570,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/../../b/../c/",
|
rawPath: "/a/../../b/../c/",
|
||||||
expectedPath: "/c/",
|
expectedPath: "/c/",
|
||||||
expectedRaw: "/c/",
|
expectedRaw: "/c/",
|
||||||
expectedRoutingPath: "/c/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -632,7 +577,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/../b/./c/../d/",
|
rawPath: "/a/../b/./c/../d/",
|
||||||
expectedPath: "/b/d/",
|
expectedPath: "/b/d/",
|
||||||
expectedRaw: "/b/d/",
|
expectedRaw: "/b/d/",
|
||||||
expectedRoutingPath: "/b/d/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -640,7 +584,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/b/../",
|
rawPath: "/a/b/../",
|
||||||
expectedPath: "/a/",
|
expectedPath: "/a/",
|
||||||
expectedRaw: "/a/",
|
expectedRaw: "/a/",
|
||||||
expectedRoutingPath: "/a/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -648,7 +591,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/%2E%2E/%2E%2E/b/",
|
rawPath: "/a/%2E%2E/%2E%2E/b/",
|
||||||
expectedPath: "/b/",
|
expectedPath: "/b/",
|
||||||
expectedRaw: "/b/",
|
expectedRaw: "/b/",
|
||||||
expectedRoutingPath: "/b/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -656,7 +598,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/%252E%252E/%252E%252E/b/",
|
rawPath: "/a/%252E%252E/%252E%252E/b/",
|
||||||
expectedPath: "/a/%2E%2E/%2E%2E/b/",
|
expectedPath: "/a/%2E%2E/%2E%2E/b/",
|
||||||
expectedRaw: "/a/%252E%252E/%252E%252E/b/",
|
expectedRaw: "/a/%252E%252E/%252E%252E/b/",
|
||||||
expectedRoutingPath: "/a/%252E%252E/%252E%252E/b/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -664,7 +605,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/foo%20bar",
|
rawPath: "/foo%20bar",
|
||||||
expectedPath: "/foo bar",
|
expectedPath: "/foo bar",
|
||||||
expectedRaw: "/foo%20bar",
|
expectedRaw: "/foo%20bar",
|
||||||
expectedRoutingPath: "/foo bar",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -672,7 +612,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/foo%2Fbar",
|
rawPath: "/foo%2Fbar",
|
||||||
expectedPath: "/foo/bar",
|
expectedPath: "/foo/bar",
|
||||||
expectedRaw: "/foo%2Fbar",
|
expectedRaw: "/foo%2Fbar",
|
||||||
expectedRoutingPath: "/foo%2Fbar",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -680,7 +619,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/foo%20%2Fbar",
|
rawPath: "/foo%20%2Fbar",
|
||||||
expectedPath: "/foo /bar",
|
expectedPath: "/foo /bar",
|
||||||
expectedRaw: "/foo%20%2Fbar",
|
expectedRaw: "/foo%20%2Fbar",
|
||||||
expectedRoutingPath: "/foo %2Fbar",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -688,7 +626,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
rawPath: "/a/..%2Fb/",
|
rawPath: "/a/..%2Fb/",
|
||||||
expectedPath: "/a/../b/",
|
expectedPath: "/a/../b/",
|
||||||
expectedRaw: "/a/..%2Fb/",
|
expectedRaw: "/a/..%2Fb/",
|
||||||
expectedRoutingPath: "/a/..%2Fb/",
|
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -706,7 +643,6 @@ func TestPathOperations(t *testing.T) {
|
||||||
assert.Equal(t, test.expectedStatus, res.StatusCode)
|
assert.Equal(t, test.expectedStatus, res.StatusCode)
|
||||||
assert.Equal(t, test.expectedPath, res.Header.Get("Path"))
|
assert.Equal(t, test.expectedPath, res.Header.Get("Path"))
|
||||||
assert.Equal(t, test.expectedRaw, res.Header.Get("RawPath"))
|
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