From 0b6438b7c0d15a5219755b3a56d3f933929c84bd Mon Sep 17 00:00:00 2001 From: GreyXor <79602273+GreyXor@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:04:05 +0100 Subject: [PATCH 1/3] Bump github.com/quic-go/quic-go to v0.57.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b3a8b15f7..52b707a34 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo. github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 - github.com/quic-go/quic-go v0.57.0 + github.com/quic-go/quic-go v0.57.1 github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac // No tag on the repo. github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 7e2d0de14..81d31eac5 100644 --- a/go.sum +++ b/go.sum @@ -1254,8 +1254,8 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++ github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 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/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac h1:wBGhHdXKICZmvAPWS8gQoMyOWDH7QAi9bU4Z1nDWnFU= github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac/go.mod h1:67sLWL17mVlO1HFROaTBmU71NB4R8UNCesFHhg0f6LQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= From 4d7d627319ee695f31e3a90d7e1ebd3af0ead081 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Dec 2025 15:10:05 +0100 Subject: [PATCH 2/3] Reject suspicious encoded characters Co-authored-by: Kevin Pollet --- docs/content/migration/v2.md | 20 ++ .../reference/static-configuration/cli-ref.md | 24 ++ .../reference/static-configuration/env-ref.md | 24 ++ .../reference/static-configuration/file.toml | 8 + .../reference/static-configuration/file.yaml | 8 + docs/content/routing/entrypoints.md | 249 ++++++++++++++++++ docs/content/security/content-length.md | 9 +- docs/content/security/request-path.md | 129 +++++++++ docs/mkdocs.yml | 1 + integration/fixtures/websocket/config.toml | 2 + pkg/api/testdata/entrypoint-bar.json | 6 +- .../testdata/entrypoint-foo-slash-bar.json | 4 +- .../testdata/entrypoints-many-lastpage.json | 22 +- pkg/api/testdata/entrypoints-page2.json | 6 +- pkg/api/testdata/entrypoints.json | 8 +- pkg/config/static/entrypoints.go | 55 +++- pkg/config/static/entrypoints_test.go | 158 +++++++++++ .../testdata/anonymized-static-config.json | 3 +- pkg/server/server_entrypoint_tcp.go | 33 +++ pkg/server/server_entrypoint_tcp_test.go | 57 ++++ 20 files changed, 804 insertions(+), 22 deletions(-) create mode 100644 docs/content/security/request-path.md diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 70261ead3..db2936b00 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -714,3 +714,23 @@ It appears that enabling MPTCP on some platforms can cause Traefik to stop with - `set tcp X.X.X.X:X->X.X.X.X:X: setsockopt: operation not supported` However, it can be re-enabled by setting the `multipathtcp` variable in the GODEBUG environment variable, see the related [go documentation](https://go.dev/doc/godebug#go-124). + +## v2.11.32 + +## Encoded Characters in Request Path + +Since `v2.11.32`, 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](../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 5bdbe09a3..1e88c2350 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -123,6 +123,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 4e3630035..945f5dfa1 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -132,6 +132,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 c6ec8e715..a69e2406e 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -55,6 +55,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 42d9274d0..99a08fccb 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -62,6 +62,14 @@ entryPoints: sans: - foobar - foobar + encodedCharacters: + allowEncodedSlash: true + allowEncodedBackSlash: true + allowEncodedNullCharacter: true + allowEncodedSemicolon: true + allowEncodedPercent: true + allowEncodedQuestionMark: true + allowEncodedHash: true encodeQuerySemicolons: true sanitizePath: true http2: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index df88cf16e..1062e6c18 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -127,6 +127,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)" @@ -152,6 +160,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" @@ -168,6 +184,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 @@ -455,6 +478,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..dbada1873 --- /dev/null +++ b/docs/content/security/request-path.md @@ -0,0 +1,129 @@ +--- +title: "Request Path Security" +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 e7a270874..e39aac44d 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -167,6 +167,7 @@ nav: - 'Elastic': 'observability/tracing/elastic.md' - 'OpenTelemetry': 'observability/tracing/opentelemetry.md' - 'Security': + - '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' - 'User Guides': diff --git a/integration/fixtures/websocket/config.toml b/integration/fixtures/websocket/config.toml index 83a499ce7..c4a25c48b 100644 --- a/integration/fixtures/websocket/config.toml +++ b/integration/fixtures/websocket/config.toml @@ -8,6 +8,8 @@ [entryPoints] [entryPoints.web] address = ":8000" + [entryPoints.web.http.encodedCharacters] + allowEncodedSlash = true [api] insecure = true 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 7fced1b85..c3ad6a42c 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -59,11 +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"` - 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. @@ -72,6 +73,50 @@ func (h *HTTPConfig) SetDefaults() { h.SanitizePath = &sanitizePath } +// 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 09e0d796d..c834c1395 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -70,7 +70,8 @@ ] } ] - } + }, + "encodedCharacters": {} } } }, diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 1036240cf..feff5f8d3 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -608,6 +608,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler = denyFragment(handler) + handler = denyEncodedCharacters(configuration.HTTP.EncodedCharacters.Map(), handler) + serverHTTP := &http.Server{ Protocols: &protocols, Handler: handler, @@ -707,6 +709,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.FromContext(req.Context()).Debugf("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 f09c9ba63..2d6bda4f3 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -429,6 +429,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{ @@ -575,6 +628,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 createHTTPServer. server, err := createHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil)) require.NoError(t, err) From 63a6172ec45331ae9d60929f7db99fd9e572f777 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Dec 2025 15:26:04 +0100 Subject: [PATCH 3/3] Prepare release v2.11.32 --- CHANGELOG.md | 14 ++++++++++++++ script/gcg/traefik-bugfix.toml | 10 +++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4835f40d..61b1f0206 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)) + ## [v2.11.31](https://github.com/traefik/traefik/tree/v2.11.31) (2025-11-13) [All Commits](https://github.com/traefik/traefik/compare/v2.11.30...v2.11.31) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 50a57a14a..5df388f43 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,14 +4,14 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.31 +# example new bugfix v2.11.32 CurrentRef = "v2.11" -PreviousRef = "v2.11.30" +PreviousRef = "v2.11.31" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.31" +FutureCurrentRefName = "v2.11.32" -ThresholdPreviousRef = 10 -ThresholdCurrentRef = 10 +ThresholdPreviousRef = 10000 +ThresholdCurrentRef = 10000 Debug = true DisplayLabel = true