diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 1e88c2350..b9fd73d71 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -123,9 +123,6 @@ Trust only forwarded headers from selected IPs. `--entrypoints..http`: HTTP configuration. -`--entrypoints..http.encodedcharacters`: -Defines which encoded characters are allowed in the request path. - `--entrypoints..http.encodedcharacters.allowencodedbackslash`: Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 945f5dfa1..51be942b0 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -132,9 +132,6 @@ HTTP/3 configuration. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__HTTP3_ADVERTISEDPORT`: UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) -`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS`: -Defines which encoded characters are allowed in the request path. - `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDBACKSLASH`: Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```) diff --git a/integration/fixtures/simple_encoded_chars.toml b/integration/fixtures/simple_encoded_chars.toml new file mode 100644 index 000000000..56f645ca3 --- /dev/null +++ b/integration/fixtures/simple_encoded_chars.toml @@ -0,0 +1,34 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.strict] + address = ":8000" + # Default: no encoded characters allowed + + [entryPoints.permissive] + address = ":8001" + [entryPoints.permissive.http.encodedCharacters] + allowEncodedSlash = true + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.sameRouter] + service = "service1" + rule = "Host(`test.localhost`)" + +[http.services] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" diff --git a/integration/simple_test.go b/integration/simple_test.go index d820ee6ee..2b85b8e2c 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1481,3 +1481,72 @@ func (s *SimpleSuite) TestSanitizePath() { } } } + +// TestEncodedCharactersDifferentEntryPoints verifies that router handler caching does not interfere with +// per-entry-point encoded characters configuration. +// The same router should behave differently on different entry points. +func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80") + + file := s.adaptFile("fixtures/simple_encoded_chars.toml", struct { + Server1 string + }{whoami1URL}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`test.localhost`)")) + require.NoError(s.T(), err) + + testCases := []struct { + desc string + request string + target string + expected int + }{ + { + desc: "Encoded slash should be REJECTED on strict entry point", + request: "GET /path%2Fwith%2Fslash HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8000", // strict entry point + expected: http.StatusBadRequest, + }, + { + desc: "Encoded slash should be ALLOWED on permissive entry point", + request: "GET /path%2Fwith%2Fslash HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8001", // permissive entry point + expected: http.StatusOK, + }, + { + desc: "Regular path should work on strict entry point", + request: "GET /regular/path HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusOK, + }, + { + desc: "Regular path should work on permissive entry point", + request: "GET /regular/path HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8001", + expected: http.StatusOK, + }, + } + + 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) + + err = conn.Close() + require.NoError(s.T(), err) + } +} diff --git a/pkg/api/testdata/entrypoint-bar.json b/pkg/api/testdata/entrypoint-bar.json index 27b6762c4..e68aa5d99 100644 --- a/pkg/api/testdata/entrypoint-bar.json +++ b/pkg/api/testdata/entrypoint-bar.json @@ -1,7 +1,5 @@ { "address": ":81", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "bar" } diff --git a/pkg/api/testdata/entrypoint-foo-slash-bar.json b/pkg/api/testdata/entrypoint-foo-slash-bar.json index 8384c00d6..5f0bcbafc 100644 --- a/pkg/api/testdata/entrypoint-foo-slash-bar.json +++ b/pkg/api/testdata/entrypoint-foo-slash-bar.json @@ -1,7 +1,5 @@ { "address": ":81", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "foo / bar" } diff --git a/pkg/api/testdata/entrypoints-many-lastpage.json b/pkg/api/testdata/entrypoints-many-lastpage.json index a50e13584..4d2917405 100644 --- a/pkg/api/testdata/entrypoints-many-lastpage.json +++ b/pkg/api/testdata/entrypoints-many-lastpage.json @@ -1,37 +1,27 @@ [ { "address": ":14", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "ep14" }, { "address": ":15", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "ep15" }, { "address": ":16", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "ep16" }, { "address": ":17", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "ep17" }, { "address": ":18", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "ep18" } ] diff --git a/pkg/api/testdata/entrypoints-page2.json b/pkg/api/testdata/entrypoints-page2.json index 89e8d649b..b98ced751 100644 --- a/pkg/api/testdata/entrypoints-page2.json +++ b/pkg/api/testdata/entrypoints-page2.json @@ -1,9 +1,7 @@ [ { "address": ":82", - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "web2" } ] diff --git a/pkg/api/testdata/entrypoints.json b/pkg/api/testdata/entrypoints.json index 5c96cbed8..d93d07bfc 100644 --- a/pkg/api/testdata/entrypoints.json +++ b/pkg/api/testdata/entrypoints.json @@ -8,9 +8,7 @@ "192.168.1.4" ] }, - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "web", "proxyProtocol": { "insecure": true, @@ -40,9 +38,7 @@ "192.168.1.40" ] }, - "http": { - "encodedCharacters": {} - }, + "http": {}, "name": "websecure", "proxyProtocol": { "insecure": true, diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index af97325d7..3bd9c4364 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -24,8 +24,9 @@ type HTTPConfiguration struct { // Model is a set of default router's values. type Model struct { - Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` - TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` + TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + DeniedEncodedPathCharacters *RouterDeniedEncodedPathCharacters `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -42,13 +43,60 @@ type Service struct { // Router holds the router configuration. type Router struct { - EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"` - Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` - Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"` - Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"` - Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"` - TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` - DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` + EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"` + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` + Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"` + Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"` + Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"` + TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` + DeniedEncodedPathCharacters RouterDeniedEncodedPathCharacters `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` +} + +// +k8s:deepcopy-gen=true + +// RouterDeniedEncodedPathCharacters configures which encoded characters are allowed in the request path. +type RouterDeniedEncodedPathCharacters 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 (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} { + characters := make(map[string]struct{}) + + if !r.AllowEncodedSlash { + characters["%2F"] = struct{}{} + characters["%2f"] = struct{}{} + } + if !r.AllowEncodedBackSlash { + characters["%5C"] = struct{}{} + characters["%5c"] = struct{}{} + } + if !r.AllowEncodedNullCharacter { + characters["%00"] = struct{}{} + } + if !r.AllowEncodedSemicolon { + characters["%3B"] = struct{}{} + characters["%3b"] = struct{}{} + } + if !r.AllowEncodedPercent { + characters["%25"] = struct{}{} + } + if !r.AllowEncodedQuestionMark { + characters["%3F"] = struct{}{} + characters["%3f"] = struct{}{} + } + if !r.AllowEncodedHash { + characters["%23"] = struct{}{} + } + + return characters } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/http_config_test.go b/pkg/config/dynamic/http_config_test.go new file mode 100644 index 000000000..c8cda437d --- /dev/null +++ b/pkg/config/dynamic/http_config_test.go @@ -0,0 +1,165 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncodedCharactersMap(t *testing.T) { + tests := []struct { + name string + config RouterDeniedEncodedPathCharacters + 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: RouterDeniedEncodedPathCharacters{ + AllowEncodedSlash: true, + }, + expected: map[string]struct{}{ + "%5C": {}, + "%5c": {}, + "%00": {}, + "%3B": {}, + "%3b": {}, + "%25": {}, + "%3F": {}, + "%3f": {}, + "%23": {}, + }, + }, + + { + name: "Exclude encoded backslash when allowed", + config: RouterDeniedEncodedPathCharacters{ + AllowEncodedBackSlash: true, + }, + expected: map[string]struct{}{ + "%2F": {}, + "%2f": {}, + "%00": {}, + "%3B": {}, + "%3b": {}, + "%25": {}, + "%3F": {}, + "%3f": {}, + "%23": {}, + }, + }, + + { + name: "Exclude encoded null character when allowed", + config: RouterDeniedEncodedPathCharacters{ + AllowEncodedNullCharacter: true, + }, + expected: map[string]struct{}{ + "%2F": {}, + "%2f": {}, + "%5C": {}, + "%5c": {}, + "%3B": {}, + "%3b": {}, + "%25": {}, + "%3F": {}, + "%3f": {}, + "%23": {}, + }, + }, + { + name: "Exclude encoded semicolon when allowed", + config: RouterDeniedEncodedPathCharacters{ + AllowEncodedSemicolon: true, + }, + expected: map[string]struct{}{ + "%2F": {}, + "%2f": {}, + "%5C": {}, + "%5c": {}, + "%00": {}, + "%25": {}, + "%3F": {}, + "%3f": {}, + "%23": {}, + }, + }, + { + name: "Exclude encoded percent when allowed", + config: RouterDeniedEncodedPathCharacters{ + AllowEncodedPercent: true, + }, + expected: map[string]struct{}{ + "%2F": {}, + "%2f": {}, + "%5C": {}, + "%5c": {}, + "%00": {}, + "%3B": {}, + "%3b": {}, + "%3F": {}, + "%3f": {}, + "%23": {}, + }, + }, + { + name: "Exclude encoded question mark when allowed", + config: RouterDeniedEncodedPathCharacters{ + AllowEncodedQuestionMark: true, + }, + expected: map[string]struct{}{ + "%2F": {}, + "%2f": {}, + "%5C": {}, + "%5c": {}, + "%00": {}, + "%3B": {}, + "%3b": {}, + "%25": {}, + "%23": {}, + }, + }, + { + name: "Exclude encoded hash when allowed", + config: RouterDeniedEncodedPathCharacters{ + 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) + }) + } +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 68d362016..d5ff74c15 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -845,6 +845,11 @@ func (in *Model) DeepCopyInto(out *Model) { *out = new(RouterTLSConfig) (*in).DeepCopyInto(*out) } + if in.DeniedEncodedPathCharacters != nil { + in, out := &in.DeniedEncodedPathCharacters, &out.DeniedEncodedPathCharacters + *out = new(RouterDeniedEncodedPathCharacters) + **out = **in + } return } @@ -1030,6 +1035,7 @@ func (in *Router) DeepCopyInto(out *Router) { *out = new(RouterTLSConfig) (*in).DeepCopyInto(*out) } + out.DeniedEncodedPathCharacters = in.DeniedEncodedPathCharacters return } @@ -1043,6 +1049,22 @@ func (in *Router) DeepCopy() *Router { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RouterDeniedEncodedPathCharacters) DeepCopyInto(out *RouterDeniedEncodedPathCharacters) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouterDeniedEncodedPathCharacters. +func (in *RouterDeniedEncodedPathCharacters) DeepCopy() *RouterDeniedEncodedPathCharacters { + if in == nil { + return nil + } + out := new(RouterDeniedEncodedPathCharacters) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RouterTCPTLSConfig) DeepCopyInto(out *RouterTCPTLSConfig) { *out = *in diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index c3ad6a42c..29f37fa13 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -59,12 +59,12 @@ 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"` - 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"` + 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"` } // SetDefaults sets the default values. @@ -84,39 +84,6 @@ type EncodedCharacters struct { 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"` diff --git a/pkg/config/static/entrypoints_test.go b/pkg/config/static/entrypoints_test.go index a412098fe..866076508 100644 --- a/pkg/config/static/entrypoints_test.go +++ b/pkg/config/static/entrypoints_test.go @@ -65,161 +65,3 @@ 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) - }) - } -} diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index de367bfae..b0ce0106b 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -212,7 +212,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str func (i *Provider) entryPointModels(cfg *dynamic.Configuration) { for name, ep := range i.staticCfg.EntryPoints { - if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil { + if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && ep.HTTP.EncodedCharacters == nil { continue } @@ -220,6 +220,18 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) { Middlewares: ep.HTTP.Middlewares, } + if ep.HTTP.EncodedCharacters != nil { + m.DeniedEncodedPathCharacters = &dynamic.RouterDeniedEncodedPathCharacters{ + AllowEncodedSlash: ep.HTTP.EncodedCharacters.AllowEncodedSlash, + AllowEncodedBackSlash: ep.HTTP.EncodedCharacters.AllowEncodedBackSlash, + AllowEncodedPercent: ep.HTTP.EncodedCharacters.AllowEncodedPercent, + AllowEncodedQuestionMark: ep.HTTP.EncodedCharacters.AllowEncodedQuestionMark, + AllowEncodedSemicolon: ep.HTTP.EncodedCharacters.AllowEncodedSemicolon, + AllowEncodedHash: ep.HTTP.EncodedCharacters.AllowEncodedHash, + AllowEncodedNullCharacter: ep.HTTP.EncodedCharacters.AllowEncodedNullCharacter, + } + } + if ep.HTTP.TLS != nil { m.TLS = &dynamic.RouterTLSConfig{ Options: ep.HTTP.TLS.Options, diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index c834c1395..09e0d796d 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -70,8 +70,7 @@ ] } ] - }, - "encodedCharacters": {} + } } } }, diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 1a1473ae1..697fe3cf8 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -164,6 +164,12 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { cp.Middlewares = append(m.Middlewares, cp.Middlewares...) + if m.DeniedEncodedPathCharacters != nil { + // As the denied encoded path characters option is not configurable at the router level, + // we can simply copy the whole structure to override the router's default config. + cp.DeniedEncodedPathCharacters = *m.DeniedEncodedPathCharacters + } + rtName := name if len(eps) > 1 { rtName = epName + "-" + name diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 519ff975b..2d46f7388 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -36,14 +36,13 @@ type serviceManager interface { // Manager A route/router manager. type Manager struct { - routerHandlers map[string]http.Handler - serviceManager serviceManager - metricsRegistry metrics.Registry - middlewaresBuilder middlewareBuilder - chainBuilder *middleware.ChainBuilder - conf *runtime.Configuration - tlsManager *tls.Manager - deniedEncodedPathCharacters map[string]map[string]struct{} + routerHandlers map[string]http.Handler + serviceManager serviceManager + metricsRegistry metrics.Registry + middlewaresBuilder middlewareBuilder + chainBuilder *middleware.ChainBuilder + conf *runtime.Configuration + tlsManager *tls.Manager } // NewManager creates a new Manager. @@ -53,17 +52,15 @@ func NewManager(conf *runtime.Configuration, chainBuilder *middleware.ChainBuilder, metricsRegistry metrics.Registry, tlsManager *tls.Manager, - deniedEncodedPathCharacters map[string]map[string]struct{}, ) *Manager { return &Manager{ - routerHandlers: make(map[string]http.Handler), - serviceManager: serviceManager, - metricsRegistry: metricsRegistry, - middlewaresBuilder: middlewaresBuilder, - chainBuilder: chainBuilder, - conf: conf, - tlsManager: tlsManager, - deniedEncodedPathCharacters: deniedEncodedPathCharacters, + routerHandlers: make(map[string]http.Handler), + serviceManager: serviceManager, + metricsRegistry: metricsRegistry, + middlewaresBuilder: middlewaresBuilder, + chainBuilder: chainBuilder, + conf: conf, + tlsManager: tlsManager, } } @@ -82,7 +79,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t for entryPointName, routers := range m.getHTTPRouters(rootCtx, entryPoints, tls) { ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName)) - handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers) + handler, err := m.buildEntryPointHandler(ctx, routers) if err != nil { log.FromContext(ctx).Error(err) continue @@ -118,7 +115,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t return entryPointHandlers } -func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo) (http.Handler, error) { +func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.RouterInfo) (http.Handler, error) { muxer, err := httpmuxer.NewMuxer() if err != nil { return nil, err @@ -135,7 +132,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str continue } - handler, err := m.buildRouterHandler(ctxRouter, entryPointName, routerName, routerConfig) + handler, err := m.buildRouterHandler(ctxRouter, routerName, routerConfig) if err != nil { routerConfig.AddError(err, true) logger.Error(err) @@ -160,7 +157,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str return chain.Then(muxer) } -func (m *Manager) buildRouterHandler(ctx context.Context, entryPointName, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) { +func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) { if handler, ok := m.routerHandlers[routerName]; ok { return handler, nil } @@ -176,7 +173,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, entryPointName, router } } - handler, err := m.buildHTTPHandler(ctx, routerConfig, entryPointName, routerName) + handler, err := m.buildHTTPHandler(ctx, routerConfig, routerName) if err != nil { return nil, err } @@ -194,7 +191,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, entryPointName, router return m.routerHandlers[routerName], nil } -func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterInfo, entryPointName, routerName string) (http.Handler, error) { +func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterInfo, routerName string) (http.Handler, error) { var qualifiedNames []string for _, name := range router.Middlewares { qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name)) @@ -232,7 +229,7 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn return denyFragment(next), nil }) chain = chain.Append(func(next http.Handler) (http.Handler, error) { - return denyEncodedPathCharacters(m.deniedEncodedPathCharacters[entryPointName], next), nil + return denyEncodedPathCharacters(router.DeniedEncodedPathCharacters.Map(), next), nil }) return chain.Extend(*mHandler).Append(tHandler).Then(sHandler) diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 7ade16d54..1c669cf09 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" - "github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/metrics" "github.com/traefik/traefik/v2/pkg/middlewares/accesslog" "github.com/traefik/traefik/v2/pkg/middlewares/capture" @@ -320,7 +319,7 @@ func TestRouterManager_Get(t *testing.T) { chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, nil) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false) @@ -427,7 +426,7 @@ func TestAccessLog(t *testing.T) { chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, nil) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false) @@ -815,7 +814,7 @@ func TestRuntimeConfiguration(t *testing.T) { tlsManager := tls.NewManager() tlsManager.UpdateConfigs(t.Context(), nil, test.tlsOptions, nil) - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, nil) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) _ = routerManager.BuildHandlers(t.Context(), entryPoints, false) _ = routerManager.BuildHandlers(t.Context(), entryPoints, true) @@ -892,7 +891,7 @@ func TestProviderOnMiddlewares(t *testing.T) { chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, nil) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) _ = routerManager.BuildHandlers(t.Context(), entryPoints, false) @@ -908,7 +907,6 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { routers map[string]*dynamic.Router services map[string]*dynamic.Service requestPath string - encodedCharacters static.EncodedCharacters expectedStatusCode int }{ { @@ -938,6 +936,9 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { EntryPoints: []string{"web"}, Rule: "PathPrefix(`/`)", Service: "service", + DeniedEncodedPathCharacters: dynamic.RouterDeniedEncodedPathCharacters{ + AllowEncodedSlash: true, + }, }, }, services: map[string]*dynamic.Service{ @@ -947,9 +948,6 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { }, }, }, - encodedCharacters: static.EncodedCharacters{ - AllowEncodedSlash: true, - }, expectedStatusCode: http.StatusBadGateway, }, { @@ -996,7 +994,6 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { Services: runtimeServices, } - deniedEncodedPathCharacters := map[string]map[string]struct{}{"web": test.encodedCharacters.Map()} roundTripperManager := service.NewRoundTripperManager() roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(conf.Services, nil, nil, roundTripperManager) @@ -1004,7 +1001,7 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(conf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, deniedEncodedPathCharacters) + routerManager := NewManager(conf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) // Build handlers ctx := t.Context() @@ -1079,7 +1076,7 @@ func BenchmarkRouterServe(b *testing.B) { chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager, nil) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false) diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 5b8162481..d0056cc27 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -24,8 +24,7 @@ type RouterFactory struct { entryPointsTCP []string entryPointsUDP []string - allowACMEByPass map[string]bool - deniedEncodedPathCharacters map[string]map[string]struct{} + allowACMEByPass map[string]bool managerFactory *service.ManagerFactory @@ -66,21 +65,15 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * } } - deniedEncodedPathCharacters := map[string]map[string]struct{}{} - for name, ep := range staticConfiguration.EntryPoints { - deniedEncodedPathCharacters[name] = ep.HTTP.EncodedCharacters.Map() - } - return &RouterFactory{ - entryPointsTCP: entryPointsTCP, - entryPointsUDP: entryPointsUDP, - managerFactory: managerFactory, - metricsRegistry: metricsRegistry, - tlsManager: tlsManager, - chainBuilder: chainBuilder, - pluginBuilder: pluginBuilder, - allowACMEByPass: allowACMEByPass, - deniedEncodedPathCharacters: deniedEncodedPathCharacters, + entryPointsTCP: entryPointsTCP, + entryPointsUDP: entryPointsUDP, + managerFactory: managerFactory, + metricsRegistry: metricsRegistry, + tlsManager: tlsManager, + chainBuilder: chainBuilder, + pluginBuilder: pluginBuilder, + allowACMEByPass: allowACMEByPass, } } @@ -93,7 +86,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder) - routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.chainBuilder, f.metricsRegistry, f.tlsManager, f.deniedEncodedPathCharacters) + routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.chainBuilder, f.metricsRegistry, f.tlsManager) handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, false) handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true) diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 9b134f2e7..3a56856b3 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -576,8 +576,10 @@ func TestPathOperations(t *testing.T) { 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 + configuration.HTTP.EncodedCharacters = &static.EncodedCharacters{ + AllowEncodedSlash: true, + AllowEncodedPercent: true, + } // Create the HTTP server using createHTTPServer. server, err := createHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))