Reject suspicious encoded characters
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
parent
0b6438b7c0
commit
4d7d627319
20 changed files with 804 additions and 22 deletions
|
|
@ -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`
|
- `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).
|
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.<name>`<br/>`.http.encodedCharacters`<br/>`.allowEncodedSlash` |
|
||||||
|
| `%5c` or `%5C` | `\` (backslash) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedBackSlash` |
|
||||||
|
| `%00` | `NULL` (null character) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedNullCharacter` |
|
||||||
|
| `%3b` or `%3B` | `;` (semicolon) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedSemicolon` |
|
||||||
|
| `%25` | `%` (percent) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedPercent` |
|
||||||
|
| `%3f` or `%3F` | `?` (question mark) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedQuestionMark` |
|
||||||
|
| `%23` | `#` (hash) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedHash` |
|
||||||
|
|
||||||
|
Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation for more details.
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,30 @@ Trust only forwarded headers from selected IPs.
|
||||||
`--entrypoints.<name>.http`:
|
`--entrypoints.<name>.http`:
|
||||||
HTTP configuration.
|
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. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedhash`:
|
||||||
|
Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodednullcharacter`:
|
||||||
|
Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedpercent`:
|
||||||
|
Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedquestionmark`:
|
||||||
|
Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedsemicolon`:
|
||||||
|
Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`--entrypoints.<name>.http.encodedcharacters.allowencodedslash`:
|
||||||
|
Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
`--entrypoints.<name>.http.encodequerysemicolons`:
|
`--entrypoints.<name>.http.encodequerysemicolons`:
|
||||||
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,30 @@ HTTP/3 configuration. (Default: ```false```)
|
||||||
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
|
||||||
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
|
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS`:
|
||||||
|
Defines which encoded characters are allowed in the request path.
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDBACKSLASH`:
|
||||||
|
Defines whether requests with encoded back slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH`:
|
||||||
|
Defines whether requests with encoded hash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDNULLCHARACTER`:
|
||||||
|
Defines whether requests with encoded null characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDPERCENT`:
|
||||||
|
Defines whether requests with encoded percent characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDQUESTIONMARK`:
|
||||||
|
Defines whether requests with encoded question mark characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSEMICOLON`:
|
||||||
|
Defines whether requests with encoded semicolon characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH`:
|
||||||
|
Defines whether requests with encoded slash characters in the path are allowed. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:
|
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:
|
||||||
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,14 @@
|
||||||
[[entryPoints.EntryPoint0.http.tls.domains]]
|
[[entryPoints.EntryPoint0.http.tls.domains]]
|
||||||
main = "foobar"
|
main = "foobar"
|
||||||
sans = ["foobar", "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]
|
[entryPoints.EntryPoint0.http2]
|
||||||
maxConcurrentStreams = 42
|
maxConcurrentStreams = 42
|
||||||
[entryPoints.EntryPoint0.http3]
|
[entryPoints.EntryPoint0.http3]
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ entryPoints:
|
||||||
sans:
|
sans:
|
||||||
- foobar
|
- foobar
|
||||||
- foobar
|
- foobar
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedBackSlash: true
|
||||||
|
allowEncodedNullCharacter: true
|
||||||
|
allowEncodedSemicolon: true
|
||||||
|
allowEncodedPercent: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
allowEncodedHash: true
|
||||||
encodeQuerySemicolons: true
|
encodeQuerySemicolons: true
|
||||||
sanitizePath: true
|
sanitizePath: true
|
||||||
http2:
|
http2:
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,14 @@ They can be defined by using a file (YAML or TOML) or CLI arguments.
|
||||||
trustedIPs:
|
trustedIPs:
|
||||||
- "127.0.0.1"
|
- "127.0.0.1"
|
||||||
- "192.168.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)"
|
```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]
|
[entryPoints.name.forwardedHeaders]
|
||||||
insecure = true
|
insecure = true
|
||||||
trustedIPs = ["127.0.0.1", "192.168.0.1"]
|
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"
|
```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.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1
|
||||||
--entryPoints.name.forwardedHeaders.insecure=true
|
--entryPoints.name.forwardedHeaders.insecure=true
|
||||||
--entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1
|
--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
|
### Address
|
||||||
|
|
@ -455,6 +478,232 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
|
||||||
--entryPoints.web.forwardedHeaders.connection=foobar
|
--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
|
### Transport
|
||||||
|
|
||||||
#### `respondingTimeouts`
|
#### `respondingTimeouts`
|
||||||
|
|
|
||||||
|
|
@ -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."
|
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):
|
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.
|
- Compare the actual byte count to the `Content-Length` header.
|
||||||
- Reject the message if the counts do not match.
|
- Reject the message if the counts do not match.
|
||||||
|
|
||||||
!!!warning
|
!!! 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.
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
129
docs/content/security/request-path.md
Normal file
129
docs/content/security/request-path.md
Normal file
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -167,6 +167,7 @@ nav:
|
||||||
- 'Elastic': 'observability/tracing/elastic.md'
|
- 'Elastic': 'observability/tracing/elastic.md'
|
||||||
- 'OpenTelemetry': 'observability/tracing/opentelemetry.md'
|
- 'OpenTelemetry': 'observability/tracing/opentelemetry.md'
|
||||||
- 'Security':
|
- 'Security':
|
||||||
|
- 'Request Path': 'security/request-path.md'
|
||||||
- 'Content-Length': 'security/content-length.md'
|
- 'Content-Length': 'security/content-length.md'
|
||||||
- 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md'
|
- 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md'
|
||||||
- 'User Guides':
|
- 'User Guides':
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
[entryPoints]
|
[entryPoints]
|
||||||
[entryPoints.web]
|
[entryPoints.web]
|
||||||
address = ":8000"
|
address = ":8000"
|
||||||
|
[entryPoints.web.http.encodedCharacters]
|
||||||
|
allowEncodedSlash = true
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|
|
||||||
6
pkg/api/testdata/entrypoint-bar.json
vendored
6
pkg/api/testdata/entrypoint-bar.json
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"address": ":81",
|
"address": ":81",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "bar"
|
"name": "bar"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"address": ":81",
|
"address": ":81",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "foo / bar"
|
"name": "foo / bar"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
22
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
|
|
@ -1,27 +1,37 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"address": ":14",
|
"address": ":14",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep14"
|
"name": "ep14"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":15",
|
"address": ":15",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep15"
|
"name": "ep15"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":16",
|
"address": ":16",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep16"
|
"name": "ep16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":17",
|
"address": ":17",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep17"
|
"name": "ep17"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": ":18",
|
"address": ":18",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "ep18"
|
"name": "ep18"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
6
pkg/api/testdata/entrypoints-page2.json
vendored
6
pkg/api/testdata/entrypoints-page2.json
vendored
|
|
@ -1,7 +1,9 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"address": ":82",
|
"address": ":82",
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "web2"
|
"name": "web2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
8
pkg/api/testdata/entrypoints.json
vendored
8
pkg/api/testdata/entrypoints.json
vendored
|
|
@ -8,7 +8,9 @@
|
||||||
"192.168.1.4"
|
"192.168.1.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"proxyProtocol": {
|
"proxyProtocol": {
|
||||||
"insecure": true,
|
"insecure": true,
|
||||||
|
|
@ -38,7 +40,9 @@
|
||||||
"192.168.1.40"
|
"192.168.1.40"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"http": {},
|
"http": {
|
||||||
|
"encodedCharacters": {}
|
||||||
|
},
|
||||||
"name": "websecure",
|
"name": "websecure",
|
||||||
"proxyProtocol": {
|
"proxyProtocol": {
|
||||||
"insecure": true,
|
"insecure": true,
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,12 @@ func (ep *EntryPoint) SetDefaults() {
|
||||||
|
|
||||||
// HTTPConfig is the HTTP configuration of an entry point.
|
// HTTPConfig is the HTTP configuration of an entry point.
|
||||||
type HTTPConfig struct {
|
type HTTPConfig struct {
|
||||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,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"`
|
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"`
|
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"`
|
EncodedCharacters EncodedCharacters `description:"Defines which encoded characters are allowed in the request path." json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,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"`
|
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.
|
// SetDefaults sets the default values.
|
||||||
|
|
@ -72,6 +73,50 @@ func (h *HTTPConfig) SetDefaults() {
|
||||||
h.SanitizePath = &sanitizePath
|
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.
|
// HTTP2Config is the HTTP2 configuration of an entry point.
|
||||||
type HTTP2Config struct {
|
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"`
|
MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"`
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,161 @@ func TestEntryPointProtocol(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodedCharactersMap(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config EncodedCharacters
|
||||||
|
expected map[string]struct{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Handles empty configuration",
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded slash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedSlash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Exclude encoded backslash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedBackSlash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Exclude encoded null character when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedNullCharacter: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded semicolon when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedSemicolon: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded percent when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedPercent: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded question mark when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedQuestionMark: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%23": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exclude encoded hash when allowed",
|
||||||
|
config: EncodedCharacters{
|
||||||
|
AllowEncodedHash: true,
|
||||||
|
},
|
||||||
|
expected: map[string]struct{}{
|
||||||
|
"%2F": {},
|
||||||
|
"%2f": {},
|
||||||
|
"%5C": {},
|
||||||
|
"%5c": {},
|
||||||
|
"%00": {},
|
||||||
|
"%3B": {},
|
||||||
|
"%3b": {},
|
||||||
|
"%25": {},
|
||||||
|
"%3F": {},
|
||||||
|
"%3f": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
result := test.config.Map()
|
||||||
|
require.Equal(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"encodedCharacters": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
||||||
|
|
||||||
handler = denyFragment(handler)
|
handler = denyFragment(handler)
|
||||||
|
|
||||||
|
handler = denyEncodedCharacters(configuration.HTTP.EncodedCharacters.Map(), handler)
|
||||||
|
|
||||||
serverHTTP := &http.Server{
|
serverHTTP := &http.Server{
|
||||||
Protocols: &protocols,
|
Protocols: &protocols,
|
||||||
Handler: handler,
|
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.
|
// 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.
|
// 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.
|
// In this case, Traefik will encode the '#' character, altering the request's intended meaning.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestNormalizePath(t *testing.T) {
|
||||||
unreservedDecoded := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
unreservedDecoded := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||||
unreserved := []string{
|
unreserved := []string{
|
||||||
|
|
@ -575,6 +628,10 @@ func TestPathOperations(t *testing.T) {
|
||||||
configuration := &static.EntryPoint{}
|
configuration := &static.EntryPoint{}
|
||||||
configuration.SetDefaults()
|
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.
|
// Create the HTTP server using createHTTPServer.
|
||||||
server, err := createHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
server, err := createHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue