diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f0c9b62..8c2951b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [v2.11.32](https://github.com/traefik/traefik/tree/v2.11.32) (2025-12-04) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.31...v2.11.32) + + **Bug fixes:** +- **[server]** Reject suspicious encoded characters ([#12360](https://github.com/traefik/traefik/pull/12360) by [rtribotte](https://github.com/rtribotte)) +- **[plugins]** Validate plugin module name ([#12291](https://github.com/traefik/traefik/pull/12291) by [kevinpollet](https://github.com/kevinpollet)) +- **[http3]** Bump github.com/quic-go/quic-go to v0.57.1 ([#12319](https://github.com/traefik/traefik/pull/12319) by [GreyXor](https://github.com/GreyXor)) +- **[http3]** Bump github.com/quic-go/quic-go to v0.57.0 ([#12308](https://github.com/traefik/traefik/pull/12308) by [GreyXor](https://github.com/GreyXor)) +- **[server]** Bump golang.org/x/crypto to v0.45.0 ([#12296](https://github.com/traefik/traefik/pull/12296) by [kevinpollet](https://github.com/kevinpollet)) + +**Documentation:** +- Update SECURITY.md to streamline information ([#12310](https://github.com/traefik/traefik/pull/12310) by [emilevauge](https://github.com/emilevauge)) +- Update SECURITY.md ([#12304](https://github.com/traefik/traefik/pull/12304) by [cwayne18](https://github.com/cwayne18)) + ## [v3.6.2](https://github.com/traefik/traefik/tree/v3.6.2) (2025-11-18) [All Commits](https://github.com/traefik/traefik/compare/v3.6.1...v3.6.2) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 1d13f463f..670d86a51 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -554,3 +554,25 @@ The KubernetesIngressNGINX Provider is no longer experimental in v3.6.2 and can 1. Remove the `kubernetesIngressNGINX` option from the experimental section 2. Configure the provider using the [kubernetesIngressNGINX Provider documentation](../reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md) + +## v3.6.3 + +### Encoded Characters in Request Path + +Starting with `v3.6.3`, for security reasons, Traefik now rejects requests with a path containing a specific set of encoded characters by default. + +When such a request is received, Traefik responds with a `400 Bad Request` status code. + +Here is the list of the encoded characters that are rejected by default, along with the corresponding configuration option to allow them: + +| Encoded Character | Character | Config option to allow the encoded character | +|-------------------|-------------------------|--------------------------------------------------------------------------------------| +| `%2f` or `%2F` | `/` (slash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSlash` | +| `%5c` or `%5C` | `\` (backslash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | +| `%00` | `NULL` (null character) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | +| `%3b` or `%3B` | `;` (semicolon) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | +| `%25` | `%` (percent) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedPercent` | +| `%3f` or `%3F` | `?` (question mark) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | +| `%23` | `#` (hash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedHash` | + +Please check out the entrypoint [encodedCharacters option](../reference/install-configuration/entrypoints.md#opt-http-encodedCharacters) documentation for more details. diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 3da346ee6..59e1794b6 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -85,6 +85,14 @@ THIS FILE MUST NOT BE EDITED BY HAND | entrypoints._name_.forwardedheaders.insecure | Trust all forwarded headers. | false | | entrypoints._name_.forwardedheaders.trustedips | Trust only forwarded headers from selected IPs. | | | entrypoints._name_.http | HTTP configuration. | | +| entrypoints._name_.http.encodedcharacters | Defines which encoded characters are allowed in the request path. | | +| entrypoints._name_.http.encodedcharacters.allowencodedbackslash | Defines whether requests with encoded back slash characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodedhash | Defines whether requests with encoded hash characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodednullcharacter | Defines whether requests with encoded null characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodedpercent | Defines whether requests with encoded percent characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodedquestionmark | Defines whether requests with encoded question mark characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodedsemicolon | Defines whether requests with encoded semicolon characters in the path are allowed. | false | +| entrypoints._name_.http.encodedcharacters.allowencodedslash | Defines whether requests with encoded slash characters in the path are allowed. | false | | entrypoints._name_.http.encodequerysemicolons | Defines whether request query semicolons should be URLEncoded. | false | | entrypoints._name_.http.maxheaderbytes | Maximum size of request headers in bytes. | 1048576 | | entrypoints._name_.http.middlewares | Default middlewares for the routers linked to the entry point. | | diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index 9ae9e4616..e0e5c17df 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -94,6 +94,14 @@ additionalArguments: | `http.redirections.`
`entryPoint.scheme`
| The target scheme to use for (permanent) redirection of all incoming requests. | https | No | | `http.redirections.`
`entryPoint.permanent`
| Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme.
The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No | | `http.redirections.`
`entryPoint.priority`
| Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No | +| `http.encodedCharacters` | Defines which encoded characters are allowed in the request path. More information [here](#encoded-characters). | false | No | +| `http.encodedCharacters.`
`allowEncodedSlash`
| Defines whether requests with encoded slash characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedBackSlash`
| Defines whether requests with encoded back slash characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedNullCharacter`
| Defines whether requests with encoded null characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedSemicolon`
| Defines whether requests with encoded semicolon characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedPercent`
| Defines whether requests with encoded percent characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedQuestionMark`
| Defines whether requests with encoded question mark characters in the path are allowed. | false | No | +| `http.encodedCharacters.`
`allowEncodedHash`
| Defines whether requests with encoded hash characters in the path are allowed. | false | No | | `http.encodeQuerySemicolons` | Enable query semicolons encoding.
Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik.
When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.
More information [here](#encodequerysemicolons). | false | No | | `http.sanitizePath` | Defines whether to enable the request path sanitization.
More information [here](#sanitizepath). | false | No | | `http.middlewares` | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point.
More information [here](#httpmiddlewares). | - | No | @@ -208,6 +216,27 @@ it can lead to unsafe routing when the `sanitizePath` option is set to `false`. | false | /./foo/../bar// | /./foo/../bar// | | true | /./foo/../bar// | /bar/ | +### Encoded Characters + +You can configure Traefik to control the handling of encoded characters in request paths for security purposes. +By default, Traefik rejects requests containing certain encoded characters that could be used in path traversal or other security attacks. + +!!! warning "Security Considerations" + + Allowing certain encoded characters may expose your application to security vulnerabilities. + +Here is the list of the encoded characters that are rejected by default: + +| Encoded Character | Character | +|-------------------|-------------------------| +| `%2f` or `%2F` | `/` (slash) | +| `%5c` or `%5C` | `\` (backslash) | +| `%00` | `NULL` (null character) | +| `%3b` or `%3B` | `;` (semicolon) | +| `%25` | `%` (percent) | +| `%3f` or `%3F` | `?` (question mark) | +| `%23` | `#` (hash) | + ### HTTP3 As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index ec0d3cc58..cdb43f1ab 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -237,6 +237,30 @@ 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```) + +`--entrypoints..http.encodedcharacters.allowencodedhash`: +Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```) + +`--entrypoints..http.encodedcharacters.allowencodednullcharacter`: +Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```) + +`--entrypoints..http.encodedcharacters.allowencodedpercent`: +Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```) + +`--entrypoints..http.encodedcharacters.allowencodedquestionmark`: +Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```) + +`--entrypoints..http.encodedcharacters.allowencodedsemicolon`: +Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```) + +`--entrypoints..http.encodedcharacters.allowencodedslash`: +Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```) + `--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 0148de541..1b7fd6e62 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -246,6 +246,30 @@ 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```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH`: +Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDNULLCHARACTER`: +Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDPERCENT`: +Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDQUESTIONMARK`: +Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSEMICOLON`: +Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH`: +Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```) + `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEQUERYSEMICOLONS`: Defines whether request query semicolons should be URLEncoded. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 10821e0c5..ed9cb0dad 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -72,6 +72,14 @@ [[entryPoints.EntryPoint0.http.tls.domains]] main = "foobar" sans = ["foobar", "foobar"] + [entryPoints.EntryPoint0.http.encodedCharacters] + allowEncodedSlash = true + allowEncodedBackSlash = true + allowEncodedNullCharacter = true + allowEncodedSemicolon = true + allowEncodedPercent = true + allowEncodedQuestionMark = true + allowEncodedHash = true [entryPoints.EntryPoint0.http2] maxConcurrentStreams = 42 [entryPoints.EntryPoint0.http3] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 21f36966f..89071c3d1 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -83,6 +83,14 @@ entryPoints: sans: - foobar - foobar + encodedCharacters: + allowEncodedSlash: true + allowEncodedBackSlash: true + allowEncodedNullCharacter: true + allowEncodedSemicolon: true + allowEncodedPercent: true + allowEncodedQuestionMark: true + allowEncodedHash: true encodeQuerySemicolons: true sanitizePath: true maxHeaderBytes: 42 diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 8f1b7a10d..ecb2a40d8 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -129,6 +129,14 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. trustedIPs: - "127.0.0.1" - "192.168.0.1" + encodedCharacters: + allowEncodedSlash: true + allowEncodedBackSlash: true + allowEncodedNullCharacter: true + allowEncodedSemicolon: true + allowEncodedPercent: true + allowEncodedQuestionMark: true + allowEncodedHash: true ``` ```toml tab="File (TOML)" @@ -156,6 +164,14 @@ They can be defined by using a file (YAML or TOML) or CLI arguments. [entryPoints.name.forwardedHeaders] insecure = true trustedIPs = ["127.0.0.1", "192.168.0.1"] + [entryPoints.name.encodedCharacters] + allowEncodedSlash = true + allowEncodedBackSlash = true + allowEncodedNullCharacter = true + allowEncodedSemicolon = true + allowEncodedPercent = true + allowEncodedQuestionMark = true + allowEncodedHash = true ``` ```bash tab="CLI" @@ -174,6 +190,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.encodedCharacters.allowEncodedSlash=true + --entryPoints.name.encodedCharacters.allowEncodedBackSlash=true + --entryPoints.name.encodedCharacters.allowEncodedNullCharacter=true + --entryPoints.name.encodedCharacters.allowEncodedSemicolon=true + --entryPoints.name.encodedCharacters.allowEncodedPercent=true + --entryPoints.name.encodedCharacters.allowEncodedQuestionMark=true + --entryPoints.name.encodedCharacters.allowEncodedHash=true ``` ### Address @@ -614,6 +637,232 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward --entryPoints.web.forwardedHeaders.connection=foobar ``` +### Encoded Characters + +You can configure Traefik to control the handling of encoded characters in request paths for security purposes. +By default, Traefik rejects requests containing certain encoded characters that could be used in path traversal or other security attacks. + +!!! warning "Security Considerations" + + Allowing certain encoded characters may expose your application to security vulnerabilities. + +??? info "`encodedCharacters.allowEncodedSlash`" + + _Optional, Default=false_ + + Controls whether requests with encoded slash characters (`%2F` or `%2f`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedSlash: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedSlash = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedSlash=true + ``` + +??? info "`encodedCharacters.allowEncodedBackSlash`" + + _Optional, Default=false_ + + Controls whether requests with encoded back slash characters (`%5C` or `%5c`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedBackSlash: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedBackSlash = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedBackSlash=true + ``` + +??? info "`encodedCharacters.allowEncodedNullCharacter`" + + _Optional, Default=false_ + + Controls whether requests with encoded null characters (`%00`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedNullCharacter: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedNullCharacter = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedNullCharacter=true + ``` + +??? info "`encodedCharacters.allowEncodedSemicolon`" + + _Optional, Default=false_ + + Controls whether requests with encoded semicolon characters (`%3B` or `%3b`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedSemicolon: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedSemicolon = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedSemicolon=true + ``` + +??? info "`encodedCharacters.allowEncodedPercent`" + + _Optional, Default=false_ + + Controls whether requests with encoded percent characters (`%25`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedPercent: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedPercent = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedPercent=true + ``` + +??? info "`encodedCharacters.allowEncodedQuestionMark`" + + _Optional, Default=false_ + + Controls whether requests with encoded question mark characters (`%3F` or `%3f`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedQuestionMark: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedQuestionMark = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedQuestionMark=true + ``` + +??? info "`encodedCharacters.allowEncodedHash`" + + _Optional, Default=false_ + + Controls whether requests with encoded hash characters (`%23`) in the path are allowed. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + encodedCharacters: + allowEncodedHash: true + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.encodedCharacters] + allowEncodedHash = true + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.encodedCharacters.allowEncodedHash=true + ``` + ### Transport #### `respondingTimeouts` diff --git a/docs/content/security/content-length.md b/docs/content/security/content-length.md index fff161881..2e6c90706 100644 --- a/docs/content/security/content-length.md +++ b/docs/content/security/content-length.md @@ -3,7 +3,8 @@ title: "Content-Length" description: "Enforce strict Content‑Length validation in Traefik by streaming or full buffering to prevent truncated or over‑long requests and responses. Read the technical documentation." --- -Traefik acts as a streaming proxy. By default, it checks each chunk of data against the `Content-Length` header as it passes it on to the backend or client. This live check blocks truncated or over‑long streams without holding the entire message. +Traefik acts as a streaming proxy. By default, it checks each chunk of data against the `Content-Length` header as it passes it on to the backend or client. +This live check blocks truncated or over‑long streams without holding the entire message. If you need Traefik to read and verify the full body before any data moves on, add the [buffering middleware](../middlewares/http/buffering.md): @@ -20,5 +21,7 @@ With buffering enabled, Traefik will: - Compare the actual byte count to the `Content-Length` header. - Reject the message if the counts do not match. -!!!warning - Buffering adds overhead. Every request and response is held in full before forwarding, which can increase memory use and latency. Use it when strict content validation is critical to your security posture. +!!! warning + + Buffering adds overhead. Every request and response is held in full before forwarding, which can increase memory use and latency. + Use it when strict content validation is critical to your security posture. diff --git a/docs/content/security/request-path.md b/docs/content/security/request-path.md new file mode 100644 index 000000000..382b10d64 --- /dev/null +++ b/docs/content/security/request-path.md @@ -0,0 +1,129 @@ +--- +title: "Request Path" +description: "Learn how Traefik processes and secures request paths through sanitization and encoded character filtering to protect against path traversal and injection attacks." +--- + +# Request Path + +Protecting Against Path-Based Attacks Through Sanitization and Filtering +{: .subtitle } + +Traefik implements multiple layers of security when processing incoming request paths. +This includes path sanitization to normalize potentially dangerous sequences and encoded character filtering to prevent attack vectors that use URL encoding. +Understanding how Traefik handles request paths is crucial for maintaining a secure routing infrastructure. + +## How Traefik Processes Request Paths + +When Traefik receives an HTTP request, it processes the request path through several security-focused stages: + +### 1. Encoded Character Filtering + +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: + +| Encoded Character | Character | +|-------------------|-------------------------| +| `%2f` or `%2F` | `/` (slash) | +| `%5c` or `%5C` | `\` (backslash) | +| `%00` | `NULL` (null character) | +| `%3b` or `%3B` | `;` (semicolon) | +| `%25` | `%` (percent) | +| `%3f` or `%3F` | `?` (question mark) | +| `%23` | `#` (hash) | + +### 2. Path Normalization + +Traefik normalizes the request path by decoding the unreserved percent-encoded characters, +as they are equivalent to their non-encoded form (according to [rfc3986#section-2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3)), +and capitalizing the percent-encoded characters (according to [rfc3986#section-6.2.2.1](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1)). + +### 3. Path Sanitization + +Traefik sanitizes request paths to prevent common attack vectors, +by removing the `..`, `.` and duplicate slash segments from the URL (according to [rfc3986#section-6.2.2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3)). + +## Path Security Configuration + +Traefik provides two main mechanisms for path security that work together to protect your applications. + +### Path Sanitization + +Path sanitization is enabled by default and helps prevent directory traversal attacks by normalizing request paths. +Configure it in the [EntryPoints](../routing/entrypoints.md#sanitizepath) HTTP section: + +```yaml tab="File (YAML)" +entryPoints: + websecure: + address: ":443" + http: + sanitizePath: true # Default: true (recommended) +``` + +```toml tab="File (TOML)" +[entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.http] + sanitizePath = true +``` + +```bash tab="CLI" +--entryPoints.websecure.address=:443 +--entryPoints.websecure.http.sanitizePath=true +``` + +**Sanitization behavior:** + +- `./foo/bar` → `/foo/bar` (removes relative current directory) +- `/foo/../bar` → `/bar` (resolves parent directory traversal) +- `/foo/bar//` → `/foo/bar/` (removes duplicate slashes) +- `/./foo/../bar//` → `/bar/` (combines all normalizations) + +### Encoded Character Filtering + +Encoded character filtering provides an additional security layer by rejecting potentially dangerous URL-encoded characters. +Configure it in the [EntryPoints](../routing/entrypoints.md#encoded-characters) HTTP section. + +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: + +```yaml tab="File (YAML)" +entryPoints: + websecure: + address: ":443" + 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) +``` + +```toml tab="File (TOML)" +[entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.encodedCharacters] + allowEncodedSlash = false + allowEncodedBackSlash = false + allowEncodedNullCharacter = false + allowEncodedSemicolon = false + allowEncodedPercent = false + allowEncodedQuestionMark = false + allowEncodedHash = false +``` + +```bash tab="CLI" +--entryPoints.websecure.address=:443 +--entryPoints.websecure.encodedCharacters.allowEncodedSlash=false +--entryPoints.websecure.encodedCharacters.allowEncodedBackSlash=false +--entryPoints.websecure.encodedCharacters.allowEncodedNullCharacter=false +--entryPoints.websecure.encodedCharacters.allowEncodedSemicolon=false +--entryPoints.websecure.encodedCharacters.allowEncodedPercent=false +--entryPoints.websecure.encodedCharacters.allowEncodedQuestionMark=false +--entryPoints.websecure.encodedCharacters.allowEncodedHash=false +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ddfdb23c4..ce2ddd16b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -358,8 +358,9 @@ nav: - 'KV' : 'reference/routing-configuration/other-providers/kv.md' - 'File' : 'reference/routing-configuration/other-providers/file.md' - 'Security': - - 'Content-Length': 'security/content-length.md' - - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' + - 'Request Path': 'security/request-path.md' + - 'Content-Length': 'security/content-length.md' + - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' - 'Deprecation Notices': - 'Releases': 'deprecation/releases.md' - 'Features': 'deprecation/features.md' diff --git a/go.mod b/go.mod index df56b4583..9eab23d87 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo. github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_model v0.6.2 - github.com/quic-go/quic-go v0.57.0 + github.com/quic-go/quic-go v0.57.1 github.com/redis/go-redis/v9 v9.8.0 github.com/rs/zerolog v1.33.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index ae687bad8..4aa9c0355 100644 --- a/go.sum +++ b/go.sum @@ -1116,8 +1116,8 @@ github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= -github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= diff --git a/integration/fixtures/websocket/config.toml b/integration/fixtures/websocket/config.toml index 5d97bcf43..a214a56d1 100644 --- a/integration/fixtures/websocket/config.toml +++ b/integration/fixtures/websocket/config.toml @@ -9,6 +9,8 @@ [entryPoints] [entryPoints.web] address = ":8000" + [entryPoints.web.http.encodedCharacters] + allowEncodedSlash = true [api] insecure = true diff --git a/pkg/api/handler_support_dump_test.go b/pkg/api/handler_support_dump_test.go index f1c4d507a..7dede6df3 100644 --- a/pkg/api/handler_support_dump_test.go +++ b/pkg/api/handler_support_dump_test.go @@ -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":`) diff --git a/pkg/api/testdata/entrypoint-bar.json b/pkg/api/testdata/entrypoint-bar.json index 897b16e00..27b6762c4 100644 --- a/pkg/api/testdata/entrypoint-bar.json +++ b/pkg/api/testdata/entrypoint-bar.json @@ -1,5 +1,7 @@ { "address": ":81", - "http": {}, + "http": { + "encodedCharacters": {} + }, "name": "bar" -} \ No newline at end of file +} diff --git a/pkg/api/testdata/entrypoint-foo-slash-bar.json b/pkg/api/testdata/entrypoint-foo-slash-bar.json index 5f0bcbafc..8384c00d6 100644 --- a/pkg/api/testdata/entrypoint-foo-slash-bar.json +++ b/pkg/api/testdata/entrypoint-foo-slash-bar.json @@ -1,5 +1,7 @@ { "address": ":81", - "http": {}, + "http": { + "encodedCharacters": {} + }, "name": "foo / bar" } diff --git a/pkg/api/testdata/entrypoints-many-lastpage.json b/pkg/api/testdata/entrypoints-many-lastpage.json index 3e0f438e5..a50e13584 100644 --- a/pkg/api/testdata/entrypoints-many-lastpage.json +++ b/pkg/api/testdata/entrypoints-many-lastpage.json @@ -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" } -] \ No newline at end of file +] diff --git a/pkg/api/testdata/entrypoints-page2.json b/pkg/api/testdata/entrypoints-page2.json index 2d674dc6d..89e8d649b 100644 --- a/pkg/api/testdata/entrypoints-page2.json +++ b/pkg/api/testdata/entrypoints-page2.json @@ -1,7 +1,9 @@ [ { "address": ":82", - "http": {}, + "http": { + "encodedCharacters": {} + }, "name": "web2" } -] \ No newline at end of file +] diff --git a/pkg/api/testdata/entrypoints.json b/pkg/api/testdata/entrypoints.json index d93d07bfc..5c96cbed8 100644 --- a/pkg/api/testdata/entrypoints.json +++ b/pkg/api/testdata/entrypoints.json @@ -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, diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 6a09d29d2..5f5935649 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -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"` diff --git a/pkg/config/static/entrypoints_test.go b/pkg/config/static/entrypoints_test.go index 866076508..a412098fe 100644 --- a/pkg/config/static/entrypoints_test.go +++ b/pkg/config/static/entrypoints_test.go @@ -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) + }) + } +} diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index afd1d19b8..d380f1692 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -82,7 +82,8 @@ ] } ] - } + }, + "encodedCharacters": {} } } }, diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 55a1f7a1a..18634f024 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -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. diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index acb3de6de..cbcb986e5 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -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) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 91d3f1ee5..9553cb549 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -10,8 +10,8 @@ PreviousRef = "v3.6.1" BaseBranch = "v3.6" FutureCurrentRefName = "v3.6.2" -ThresholdPreviousRef = 10 -ThresholdCurrentRef = 10 +ThresholdPreviousRef = 10000 +ThresholdCurrentRef = 10000 Debug = true DisplayLabel = true