Merge branch v2.11 into v3.6
This commit is contained in:
commit
61ad0f13e8
27 changed files with 866 additions and 31 deletions
|
|
@ -76,7 +76,7 @@ func TestHandler_SupportDump(t *testing.T) {
|
|||
assert.Contains(t, string(files["version.json"]), `"version":"dev"`)
|
||||
|
||||
// Verify static config contains entry points
|
||||
assert.Contains(t, string(files["static-config.json"]), `"entryPoints":{"web":{"address":"xxxx","http":{}}}`)
|
||||
assert.Contains(t, string(files["static-config.json"]), `"entryPoints":{"web":{"address":"xxxx","http":{"encodedCharacters":{}}}`)
|
||||
|
||||
// Verify runtime config contains services
|
||||
assert.Contains(t, string(files["runtime-config.json"]), `"services":`)
|
||||
|
|
|
|||
6
pkg/api/testdata/entrypoint-bar.json
vendored
6
pkg/api/testdata/entrypoint-bar.json
vendored
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"address": ":81",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"address": ":81",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "foo / bar"
|
||||
}
|
||||
|
|
|
|||
22
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
22
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
|
|
@ -1,27 +1,37 @@
|
|||
[
|
||||
{
|
||||
"address": ":14",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "ep14"
|
||||
},
|
||||
{
|
||||
"address": ":15",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "ep15"
|
||||
},
|
||||
{
|
||||
"address": ":16",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "ep16"
|
||||
},
|
||||
{
|
||||
"address": ":17",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "ep17"
|
||||
},
|
||||
{
|
||||
"address": ":18",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "ep18"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
6
pkg/api/testdata/entrypoints-page2.json
vendored
6
pkg/api/testdata/entrypoints-page2.json
vendored
|
|
@ -1,7 +1,9 @@
|
|||
[
|
||||
{
|
||||
"address": ":82",
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "web2"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
8
pkg/api/testdata/entrypoints.json
vendored
8
pkg/api/testdata/entrypoints.json
vendored
|
|
@ -8,7 +8,9 @@
|
|||
"192.168.1.4"
|
||||
]
|
||||
},
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "web",
|
||||
"proxyProtocol": {
|
||||
"insecure": true,
|
||||
|
|
@ -38,7 +40,9 @@
|
|||
"192.168.1.40"
|
||||
]
|
||||
},
|
||||
"http": {},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"name": "websecure",
|
||||
"proxyProtocol": {
|
||||
"insecure": true,
|
||||
|
|
|
|||
|
|
@ -65,12 +65,13 @@ func (ep *EntryPoint) SetDefaults() {
|
|||
|
||||
// HTTPConfig is the HTTP configuration of an entry point.
|
||||
type HTTPConfig struct {
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"`
|
||||
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
||||
MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
EncodedCharacters EncodedCharacters `description:"Defines which encoded characters are allowed in the request path." json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,omitempty" export:"true"`
|
||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty" export:"true"`
|
||||
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
||||
MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
|
|
@ -80,6 +81,50 @@ func (c *HTTPConfig) SetDefaults() {
|
|||
c.MaxHeaderBytes = http.DefaultMaxHeaderBytes
|
||||
}
|
||||
|
||||
// EncodedCharacters configures which encoded characters are allowed in the request path.
|
||||
type EncodedCharacters struct {
|
||||
AllowEncodedSlash bool `description:"Defines whether requests with encoded slash characters in the path are allowed." json:"allowEncodedSlash,omitempty" toml:"allowEncodedSlash,omitempty" yaml:"allowEncodedSlash,omitempty" export:"true"`
|
||||
AllowEncodedBackSlash bool `description:"Defines whether requests with encoded back slash characters in the path are allowed." json:"allowEncodedBackSlash,omitempty" toml:"allowEncodedBackSlash,omitempty" yaml:"allowEncodedBackSlash,omitempty" export:"true"`
|
||||
AllowEncodedNullCharacter bool `description:"Defines whether requests with encoded null characters in the path are allowed." json:"allowEncodedNullCharacter,omitempty" toml:"allowEncodedNullCharacter,omitempty" yaml:"allowEncodedNullCharacter,omitempty" export:"true"`
|
||||
AllowEncodedSemicolon bool `description:"Defines whether requests with encoded semicolon characters in the path are allowed." json:"allowEncodedSemicolon,omitempty" toml:"allowEncodedSemicolon,omitempty" yaml:"allowEncodedSemicolon,omitempty" export:"true"`
|
||||
AllowEncodedPercent bool `description:"Defines whether requests with encoded percent characters in the path are allowed." json:"allowEncodedPercent,omitempty" toml:"allowEncodedPercent,omitempty" yaml:"allowEncodedPercent,omitempty" export:"true"`
|
||||
AllowEncodedQuestionMark bool `description:"Defines whether requests with encoded question mark characters in the path are allowed." json:"allowEncodedQuestionMark,omitempty" toml:"allowEncodedQuestionMark,omitempty" yaml:"allowEncodedQuestionMark,omitempty" export:"true"`
|
||||
AllowEncodedHash bool `description:"Defines whether requests with encoded hash characters in the path are allowed." json:"allowEncodedHash,omitempty" toml:"allowEncodedHash,omitempty" yaml:"allowEncodedHash,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// Map returns a map of unallowed encoded characters.
|
||||
func (h *EncodedCharacters) Map() map[string]struct{} {
|
||||
characters := make(map[string]struct{})
|
||||
|
||||
if !h.AllowEncodedSlash {
|
||||
characters["%2F"] = struct{}{}
|
||||
characters["%2f"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedBackSlash {
|
||||
characters["%5C"] = struct{}{}
|
||||
characters["%5c"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedNullCharacter {
|
||||
characters["%00"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedSemicolon {
|
||||
characters["%3B"] = struct{}{}
|
||||
characters["%3b"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedPercent {
|
||||
characters["%25"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedQuestionMark {
|
||||
characters["%3F"] = struct{}{}
|
||||
characters["%3f"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedHash {
|
||||
characters["%23"] = struct{}{}
|
||||
}
|
||||
|
||||
return characters
|
||||
}
|
||||
|
||||
// HTTP2Config is the HTTP2 configuration of an entry point.
|
||||
type HTTP2Config struct {
|
||||
MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"`
|
||||
|
|
|
|||
|
|
@ -65,3 +65,161 @@ func TestEntryPointProtocol(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodedCharactersMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config EncodedCharacters
|
||||
expected map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "Handles empty configuration",
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded slash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded backslash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedBackSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded null character when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedNullCharacter: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded semicolon when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedSemicolon: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded percent when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedPercent: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded question mark when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedQuestionMark: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded hash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedHash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := test.config.Map()
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"encodedCharacters": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -685,6 +685,8 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
|
|||
|
||||
handler = denyFragment(handler)
|
||||
|
||||
handler = denyEncodedCharacters(configuration.HTTP.EncodedCharacters.Map(), handler)
|
||||
|
||||
serverHTTP := &http.Server{
|
||||
Protocols: &protocols,
|
||||
Handler: handler,
|
||||
|
|
@ -787,6 +789,37 @@ func encodeQuerySemicolons(h http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
// denyEncodedCharacters reject the request if the escaped path contains encoded characters.
|
||||
func denyEncodedCharacters(encodedCharacters map[string]struct{}, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
escapedPath := req.URL.EscapedPath()
|
||||
|
||||
for i := 0; i < len(escapedPath); i++ {
|
||||
if 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
|
||||
}
|
||||
|
||||
// This rejects a request with a path containing the given encoded characters.
|
||||
if _, exists := encodedCharacters[escapedPath[i:i+3]]; exists {
|
||||
log.Debug().Msgf("Rejecting request because it contains encoded character %s in the URL path: %s", escapedPath[i:i+3], escapedPath)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
i += 2
|
||||
}
|
||||
|
||||
h.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
// When go receives an HTTP request, it assumes the absence of fragment URL.
|
||||
// However, it is still possible to send a fragment in the request.
|
||||
// In this case, Traefik will encode the '#' character, altering the request's intended meaning.
|
||||
|
|
|
|||
|
|
@ -428,6 +428,59 @@ func TestSanitizePath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDenyEncodedCharacters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoded map[string]struct{}
|
||||
url string
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "Rejects disallowed characters",
|
||||
encoded: map[string]struct{}{
|
||||
"%0A": {},
|
||||
"%0D": {},
|
||||
},
|
||||
url: "http://example.com/foo%0Abar",
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Allows valid paths",
|
||||
encoded: map[string]struct{}{
|
||||
"%0A": {},
|
||||
"%0D": {},
|
||||
},
|
||||
url: "http://example.com/foo%20bar",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Handles empty path",
|
||||
encoded: map[string]struct{}{
|
||||
"%0A": {},
|
||||
},
|
||||
url: "http://example.com/",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := denyEncodedCharacters(test.encoded, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(res, req)
|
||||
|
||||
assert.Equal(t, test.wantStatus, res.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePath(t *testing.T) {
|
||||
unreservedDecoded := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||
unreserved := []string{
|
||||
|
|
@ -525,6 +578,10 @@ func TestPathOperations(t *testing.T) {
|
|||
configuration := &static.EntryPoint{}
|
||||
configuration.SetDefaults()
|
||||
|
||||
// We need to allow some of the suspicious encoded characters to test the path operations in case they are authorized.
|
||||
configuration.HTTP.EncodedCharacters.AllowEncodedSlash = true
|
||||
configuration.HTTP.EncodedCharacters.AllowEncodedPercent = true
|
||||
|
||||
// Create the HTTP server using newHTTPServer.
|
||||
server, err := newHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue