From adf47fba316be4fa639ad980e079e5ecbc6b0e33 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:16:04 +0100 Subject: [PATCH] Make encoded character options opt-in --- cmd/traefik/traefik.go | 8 +- docs/content/migration/v2.md | 27 +++++ .../reference/static-configuration/cli-ref.md | 14 +-- .../reference/static-configuration/env-ref.md | 14 +-- docs/content/routing/entrypoints.md | 105 +++++++++--------- docs/content/security/request-path.md | 23 ++-- .../fixtures/simple_encoded_chars.toml | 10 +- integration/fixtures/websocket/config.toml | 2 - integration/simple_test.go | 12 ++ pkg/config/dynamic/http_config.go | 16 +-- pkg/config/dynamic/zz_generated.deepcopy.go | 6 +- pkg/config/static/entrypoints.go | 10 ++ pkg/server/aggregator.go | 2 +- pkg/server/router/deny.go | 19 ---- pkg/server/router/deny_test.go | 36 ------ pkg/server/router/router.go | 13 +-- pkg/server/router/router_test.go | 27 +---- pkg/server/server_entrypoint_tcp.go | 20 ++++ pkg/server/server_entrypoint_tcp_test.go | 36 ++++++ 19 files changed, 221 insertions(+), 179 deletions(-) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 7900bb21f..1a4de5a39 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -87,10 +87,10 @@ Complete documentation is available at https://traefik.io`, func runCmd(staticConfiguration *static.Configuration) error { configureLogging(staticConfiguration) - // Display warning to advertise for new behavior of rejecting encoded characters in the request path. - // Deprecated: this has to be removed in the next minor/major version. - log.WithoutContext().Warnf("Starting with v2.11.32, Traefik now rejects some encoded characters in the request path by default. " + - "Refer to the documentation for more details: https://doc.traefik.io/traefik/v2.11/migration/v2/#encoded-characters-in-request-path") + log.WithoutContext().Warn("Traefik can reject some encoded characters in the request path." + + "When your backend is not fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986)," + + "it is recommended to set these options to `false` to avoid split-view situation." + + "Refer to the documentation for more details: https://doc.traefik.io/traefik/v2.11/migration/v2/#encoded-characters-configuration-default-values") http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index a7fa96ffd..325e2ac3b 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -737,3 +737,30 @@ Note: This check is not done against query parameters, but only against the request path as defined in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3). Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation for more details. + +## v2.11.35 + +### Encoded Characters Configuration Default Values + +Since `v2.11.35`, the options for encoded characters now have a `true` default value. +This means that Traefik will not reject requests with a path containing a specific set of encoded characters by default. +It is now up to the users to configure the security hardening of encoded characters. + +Here is the list of the encoded characters that can be configured to `false` to disallow them: + +| Encoded Character | Character | Config options | Default value | +|-------------------|-------------------------|--------------------------------------------------------------------------------------|---------------| +| `%2f` or `%2F` | `/` (slash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSlash` | `true` | +| `%5c` or `%5C` | `\` (backslash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | `true` | +| `%00` | `NULL` (null character) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | `true` | +| `%3b` or `%3B` | `;` (semicolon) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | `true` | +| `%25` | `%` (percent) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedPercent` | `true` | +| `%3f` or `%3F` | `?` (question mark) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | `true` | +| `%23` | `#` (hash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedHash` | `true` | + +Note: This check is not done against query parameters, +but only against the request path as defined +in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3). + +Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation +for more details. diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index b9fd73d71..cec0dafd3 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -124,25 +124,25 @@ Trust only forwarded headers from selected IPs. HTTP configuration. `--entrypoints..http.encodedcharacters.allowencodedbackslash`: -Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodedhash`: -Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded hash characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodednullcharacter`: -Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded null characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodedpercent`: -Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded percent characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodedquestionmark`: -Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodedsemicolon`: -Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodedcharacters.allowencodedslash`: -Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded slash characters in the path are allowed. (Default: ```true```) `--entrypoints..http.encodequerysemicolons`: Defines whether request query semicolons should be URLEncoded. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 51be942b0..6947150fa 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -133,25 +133,25 @@ HTTP/3 configuration. (Default: ```false```) UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDBACKSLASH`: -Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH`: -Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded hash characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDNULLCHARACTER`: -Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded null characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDPERCENT`: -Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded percent characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDQUESTIONMARK`: -Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSEMICOLON`: -Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH`: -Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```) +Defines whether requests with encoded slash characters in the path are allowed. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEQUERYSEMICOLONS`: Defines whether request query semicolons should be URLEncoded. (Default: ```false```) diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 02ad3d79d..d6126df56 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -129,13 +129,13 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. - "192.168.0.1" http: encodedCharacters: - allowEncodedSlash: true - allowEncodedBackSlash: true - allowEncodedNullCharacter: true - allowEncodedSemicolon: true - allowEncodedPercent: true - allowEncodedQuestionMark: true - allowEncodedHash: true + allowEncodedSlash: false + allowEncodedBackSlash: false + allowEncodedNullCharacter: false + allowEncodedSemicolon: false + allowEncodedPercent: false + allowEncodedQuestionMark: false + allowEncodedHash: false ``` ```toml tab="File (TOML)" @@ -162,13 +162,13 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. insecure = true trustedIPs = ["127.0.0.1", "192.168.0.1"] [entryPoints.name.http.encodedCharacters] - allowEncodedSlash = true - allowEncodedBackSlash = true - allowEncodedNullCharacter = true - allowEncodedSemicolon = true - allowEncodedPercent = true - allowEncodedQuestionMark = true - allowEncodedHash = true + allowEncodedSlash = false + allowEncodedBackSlash = false + allowEncodedNullCharacter = false + allowEncodedSemicolon = false + allowEncodedPercent = false + allowEncodedQuestionMark = false + allowEncodedHash = false ``` ```bash tab="CLI" @@ -185,13 +185,13 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. --entryPoints.name.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1 --entryPoints.name.forwardedHeaders.insecure=true --entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1 - --entryPoints.name.http.encodedCharacters.allowEncodedSlash=true - --entryPoints.name.http.encodedCharacters.allowEncodedBackSlash=true - --entryPoints.name.http.encodedCharacters.allowEncodedNullCharacter=true - --entryPoints.name.http.encodedCharacters.allowEncodedSemicolon=true - --entryPoints.name.http.encodedCharacters.allowEncodedPercent=true - --entryPoints.name.http.encodedCharacters.allowEncodedQuestionMark=true - --entryPoints.name.http.encodedCharacters.allowEncodedHash=true + --entryPoints.name.http.encodedCharacters.allowEncodedSlash=false + --entryPoints.name.http.encodedCharacters.allowEncodedBackSlash=false + --entryPoints.name.http.encodedCharacters.allowEncodedNullCharacter=false + --entryPoints.name.http.encodedCharacters.allowEncodedSemicolon=false + --entryPoints.name.http.encodedCharacters.allowEncodedPercent=false + --entryPoints.name.http.encodedCharacters.allowEncodedQuestionMark=false + --entryPoints.name.http.encodedCharacters.allowEncodedHash=false ``` ### Address @@ -1021,20 +1021,21 @@ entryPoints: ### Encoded Characters You can configure Traefik to control the handling of encoded characters in request paths for security purposes. -By default, Traefik rejects requests with path containing certain encoded characters that could be used in path traversal or other security attacks. +By default, Traefik do not reject requests with path containing certain encoded characters that could be used in path traversal or other security attacks. !!! info This check is not done against the request query parameters, but only against the request path as defined in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3). -!!! warning "Security Considerations" +!!! info "Security Considerations" - Allowing certain encoded characters may expose your application to security vulnerabilities. + When your backend is not fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) and notably decode encoded reserved characters in the requets path, + it is recommended to set these options to `false` to avoid split-view situation and helps prevent path traversal attacks or other malicious attempts to bypass security controls. ??? info "`encodedCharacters.allowEncodedSlash`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded slash characters (`%2F` or `%2f`) in the path are allowed. @@ -1045,7 +1046,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedSlash: true + allowEncodedSlash: false ``` ```toml tab="File (TOML)" @@ -1055,18 +1056,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedSlash = true + allowEncodedSlash = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedSlash=true + --entryPoints.web.http.encodedCharacters.allowEncodedSlash=false ``` ??? info "`encodedCharacters.allowEncodedBackSlash`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded back slash characters (`%5C` or `%5c`) in the path are allowed. @@ -1077,7 +1078,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedBackSlash: true + allowEncodedBackSlash: false ``` ```toml tab="File (TOML)" @@ -1087,18 +1088,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedBackSlash = true + allowEncodedBackSlash = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedBackSlash=true + --entryPoints.web.http.encodedCharacters.allowEncodedBackSlash=false ``` ??? info "`encodedCharacters.allowEncodedNullCharacter`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded null characters (`%00`) in the path are allowed. @@ -1109,7 +1110,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedNullCharacter: true + allowEncodedNullCharacter: false ``` ```toml tab="File (TOML)" @@ -1119,18 +1120,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedNullCharacter = true + allowEncodedNullCharacter = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedNullCharacter=true + --entryPoints.web.http.encodedCharacters.allowEncodedNullCharacter=false ``` ??? info "`encodedCharacters.allowEncodedSemicolon`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded semicolon characters (`%3B` or `%3b`) in the path are allowed. @@ -1141,7 +1142,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedSemicolon: true + allowEncodedSemicolon: false ``` ```toml tab="File (TOML)" @@ -1151,18 +1152,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedSemicolon = true + allowEncodedSemicolon = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedSemicolon=true + --entryPoints.web.http.encodedCharacters.allowEncodedSemicolon=false ``` ??? info "`encodedCharacters.allowEncodedPercent`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded percent characters (`%25`) in the path are allowed. @@ -1173,7 +1174,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedPercent: true + allowEncodedPercent: false ``` ```toml tab="File (TOML)" @@ -1183,18 +1184,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedPercent = true + allowEncodedPercent = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedPercent=true + --entryPoints.web.http.encodedCharacters.allowEncodedPercent=false ``` ??? info "`encodedCharacters.allowEncodedQuestionMark`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded question mark characters (`%3F` or `%3f`) in the path are allowed. @@ -1205,7 +1206,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedQuestionMark: true + allowEncodedQuestionMark: false ``` ```toml tab="File (TOML)" @@ -1215,18 +1216,18 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedQuestionMark = true + allowEncodedQuestionMark = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedQuestionMark=true + --entryPoints.web.http.encodedCharacters.allowEncodedQuestionMark=false ``` ??? info "`encodedCharacters.allowEncodedHash`" - _Optional, Default=false_ + _Optional, Default=true_ Controls whether requests with encoded hash characters (`%23`) in the path are allowed. @@ -1237,7 +1238,7 @@ By default, Traefik rejects requests with path containing certain encoded charac address: ":80" http: encodedCharacters: - allowEncodedHash: true + allowEncodedHash: false ``` ```toml tab="File (TOML)" @@ -1247,13 +1248,13 @@ By default, Traefik rejects requests with path containing certain encoded charac address = ":80" [entryPoints.web.http.encodedCharacters] - allowEncodedHash = true + allowEncodedHash = false ``` ```bash tab="CLI" ## Static configuration --entryPoints.web.address=:80 - --entryPoints.web.http.encodedCharacters.allowEncodedHash=true + --entryPoints.web.http.encodedCharacters.allowEncodedHash=false ``` ### SanitizePath diff --git a/docs/content/security/request-path.md b/docs/content/security/request-path.md index 464391356..fe88c3142 100644 --- a/docs/content/security/request-path.md +++ b/docs/content/security/request-path.md @@ -20,7 +20,7 @@ When Traefik receives an HTTP request, it processes the request path through sev Traefik inspects the path for potentially dangerous encoded characters and rejects requests containing them unless explicitly allowed. -Here is the list of the encoded characters that are rejected by default: +Here is the list of the encoded characters that are allowed by default: | Encoded Character | Character | |-------------------|-------------------------| @@ -87,7 +87,12 @@ Configure it in the [EntryPoints](../routing/entrypoints.md#encoded-characters) This filtering occurs before path sanitization and catches attack attempts that use encoding to bypass other security controls. -All encoded character filtering is enabled by default (`false` means encoded characters are rejected), providing maximum security: +All encoded character filtering is disabled by default (`true` means encoded characters are allowed). + +!!! info "Security Considerations" + + When your backend is not fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) and notably decode encoded reserved characters in the requets path, + it is recommended to set these options to `false` to avoid split-view situation and helps prevent path traversal attacks or other malicious attempts to bypass security controls. ```yaml tab="File (YAML)" entryPoints: @@ -95,13 +100,13 @@ entryPoints: address: ":443" http: encodedCharacters: - allowEncodedSlash: false # %2F - Default: false (RECOMMENDED) - allowEncodedBackSlash: false # %5C - Default: false (RECOMMENDED) - allowEncodedNullCharacter: false # %00 - Default: false (RECOMMENDED) - allowEncodedSemicolon: false # %3B - Default: false (RECOMMENDED) - allowEncodedPercent: false # %25 - Default: false (RECOMMENDED) - allowEncodedQuestionMark: false # %3F - Default: false (RECOMMENDED) - allowEncodedHash: false # %23 - Default: false (RECOMMENDED) + allowEncodedSlash: false # %2F - Default: true + allowEncodedBackSlash: false # %5C - Default: true + allowEncodedNullCharacter: false # %00 - Default: true + allowEncodedSemicolon: false # %3B - Default: true + allowEncodedPercent: false # %25 - Default: true + allowEncodedQuestionMark: false # %3F - Default: true + allowEncodedHash: false # %23 - Default: true ``` ```toml tab="File (TOML)" diff --git a/integration/fixtures/simple_encoded_chars.toml b/integration/fixtures/simple_encoded_chars.toml index 56f645ca3..669c60b9b 100644 --- a/integration/fixtures/simple_encoded_chars.toml +++ b/integration/fixtures/simple_encoded_chars.toml @@ -8,12 +8,18 @@ [entryPoints] [entryPoints.strict] address = ":8000" - # Default: no encoded characters allowed + [entryPoints.strict.http.encodedCharacters] + allowEncodedSlash = false [entryPoints.permissive] address = ":8001" + # No config, default values should apply + + [entryPoints.permissive2] + address = ":8002" + # No config for allowEncodedSlash, default value is effectively true [entryPoints.permissive.http.encodedCharacters] - allowEncodedSlash = true + allowEncodedBackSlash = false [api] insecure = true diff --git a/integration/fixtures/websocket/config.toml b/integration/fixtures/websocket/config.toml index c4a25c48b..83a499ce7 100644 --- a/integration/fixtures/websocket/config.toml +++ b/integration/fixtures/websocket/config.toml @@ -8,8 +8,6 @@ [entryPoints] [entryPoints.web] address = ":8000" - [entryPoints.web.http.encodedCharacters] - allowEncodedSlash = true [api] insecure = true diff --git a/integration/simple_test.go b/integration/simple_test.go index 2b85b8e2c..c2f1392c4 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1520,6 +1520,12 @@ func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { target: "127.0.0.1:8001", // permissive entry point expected: http.StatusOK, }, + { + desc: "Encoded slash should be ALLOWED on permissive2 entry point", + request: "GET /path%2Fwith%2Fslash HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8002", // permissive2 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", @@ -1532,6 +1538,12 @@ func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { target: "127.0.0.1:8001", expected: http.StatusOK, }, + { + desc: "Regular path should work on permissive2 entry point", + request: "GET /regular/path HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8002", + expected: http.StatusOK, + }, } for _, test := range testCases { diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 3bd9c4364..d397d2306 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -43,14 +43,14 @@ 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:"-"` - DeniedEncodedPathCharacters RouterDeniedEncodedPathCharacters `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:"-" kv:"-"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 068300ab8..b8f68c946 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1035,7 +1035,11 @@ func (in *Router) DeepCopyInto(out *Router) { *out = new(RouterTLSConfig) (*in).DeepCopyInto(*out) } - out.DeniedEncodedPathCharacters = in.DeniedEncodedPathCharacters + if in.DeniedEncodedPathCharacters != nil { + in, out := &in.DeniedEncodedPathCharacters, &out.DeniedEncodedPathCharacters + *out = new(RouterDeniedEncodedPathCharacters) + **out = **in + } return } diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 29f37fa13..ad9863643 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -84,6 +84,16 @@ 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"` } +func (ec *EncodedCharacters) SetDefaults() { + ec.AllowEncodedSlash = true + ec.AllowEncodedBackSlash = true + ec.AllowEncodedNullCharacter = true + ec.AllowEncodedSemicolon = true + ec.AllowEncodedPercent = true + ec.AllowEncodedQuestionMark = true + ec.AllowEncodedHash = true +} + // 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/server/aggregator.go b/pkg/server/aggregator.go index 697fe3cf8..28d07dda9 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -167,7 +167,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { 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 + cp.DeniedEncodedPathCharacters = m.DeniedEncodedPathCharacters } rtName := name diff --git a/pkg/server/router/deny.go b/pkg/server/router/deny.go index 9c64af119..423fdb696 100644 --- a/pkg/server/router/deny.go +++ b/pkg/server/router/deny.go @@ -2,29 +2,10 @@ package router import ( "net/http" - "strings" "github.com/traefik/traefik/v2/pkg/log" ) -// denyFragment rejects the request if the URL path contains a fragment (hash character). -// 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. -// To avoid this behavior, the following function rejects requests that include a fragment in the URL. -func denyFragment(h http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if strings.Contains(req.URL.RawPath, "#") { - log.WithoutContext().Debugf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath) - rw.WriteHeader(http.StatusBadRequest) - - return - } - - h.ServeHTTP(rw, req) - }) -} - // denyEncodedPathCharacters reject the request if the escaped path contains encoded characters in the given list. func denyEncodedPathCharacters(encodedCharacters map[string]struct{}, h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { diff --git a/pkg/server/router/deny_test.go b/pkg/server/router/deny_test.go index 19ece6013..de17fe5fc 100644 --- a/pkg/server/router/deny_test.go +++ b/pkg/server/router/deny_test.go @@ -8,42 +8,6 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_denyFragment(t *testing.T) { - tests := []struct { - name string - url string - wantStatus int - }{ - { - name: "Rejects fragment character", - url: "http://example.com/#", - wantStatus: http.StatusBadRequest, - }, - { - name: "Allows without fragment", - url: "http://example.com/", - wantStatus: http.StatusOK, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - handler := denyFragment(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 Test_denyEncodedPathCharacters(t *testing.T) { tests := []struct { name string diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 2d46f7388..f01210bfa 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -223,14 +223,11 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn chain = chain.Append(denyrouterrecursion.WrapHandler(routerName)) } - // Here we are adding deny handlers for encoded path characters and fragment. - // Deny handler are only added for root routers, child routers are protected by their parent router deny handlers. - chain = chain.Append(func(next http.Handler) (http.Handler, error) { - return denyFragment(next), nil - }) - chain = chain.Append(func(next http.Handler) (http.Handler, error) { - return denyEncodedPathCharacters(router.DeniedEncodedPathCharacters.Map(), next), nil - }) + if router.DeniedEncodedPathCharacters != nil { + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + 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 1c669cf09..add135bc5 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -910,13 +910,16 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { expectedStatusCode int }{ { - desc: "unallowed request with encoded slash", + desc: "disallow request with encoded slash", requestPath: "/foo%2F", routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, Rule: "PathPrefix(`/`)", Service: "service", + DeniedEncodedPathCharacters: &dynamic.RouterDeniedEncodedPathCharacters{ + AllowEncodedSlash: false, + }, }, }, services: map[string]*dynamic.Service{ @@ -936,9 +939,6 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { EntryPoints: []string{"web"}, Rule: "PathPrefix(`/`)", Service: "service", - DeniedEncodedPathCharacters: dynamic.RouterDeniedEncodedPathCharacters{ - AllowEncodedSlash: true, - }, }, }, services: map[string]*dynamic.Service{ @@ -950,25 +950,6 @@ func TestManager_BuildHandlers_Deny(t *testing.T) { }, expectedStatusCode: http.StatusBadGateway, }, - { - desc: "unallowed request with fragment", - requestPath: "/foo#", - routers: map[string]*dynamic.Router{ - "parent": { - EntryPoints: []string{"web"}, - Rule: "PathPrefix(`/`)", - Service: "service", - }, - }, - services: map[string]*dynamic.Service{ - "service": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{{URL: "http://localhost:8080"}}, - }, - }, - }, - expectedStatusCode: http.StatusBadRequest, - }, } for _, test := range testCases { diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 51b946c09..47d5c941c 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -606,6 +606,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler = normalizePath(handler) + handler = denyFragment(handler) + serverHTTP := &http.Server{ Protocols: &protocols, Handler: handler, @@ -685,6 +687,24 @@ func (t *trackedConnection) Close() error { return t.WriteCloser.Close() } +// denyFragment rejects the request if the URL path contains a fragment (hash character). +// 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. +// To avoid this behavior, the following function rejects requests that include a fragment in the URL. +func denyFragment(h http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if strings.Contains(req.URL.RawPath, "#") { + log.WithoutContext().Debugf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath) + rw.WriteHeader(http.StatusBadRequest) + + return + } + + h.ServeHTTP(rw, req) + }) +} + // This function is inspired by http.AllowQuerySemicolons. func encodeQuerySemicolons(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 3a56856b3..dbcde6b00 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -388,6 +388,42 @@ func TestKeepAliveH2c(t *testing.T) { require.Contains(t, err.Error(), "use of closed network connection") } +func Test_denyFragment(t *testing.T) { + tests := []struct { + name string + url string + wantStatus int + }{ + { + name: "Rejects fragment character", + url: "http://example.com/#", + wantStatus: http.StatusBadRequest, + }, + { + name: "Allows without fragment", + url: "http://example.com/", + wantStatus: http.StatusOK, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + handler := denyFragment(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 TestSanitizePath(t *testing.T) { tests := []struct { path string