mirror of
https://github.com/containous/traefik.git
synced 2025-12-27 00:23:53 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90ce858347 | ||
|
|
8ebab1b243 | ||
|
|
5fe10e2098 | ||
|
|
fd36de5a0a | ||
|
|
60b19b7b81 | ||
|
|
e0e49533ab | ||
|
|
351dcbd186 | ||
|
|
7f40f3cd58 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,7 +1,18 @@
|
||||
## [v2.11.33](https://github.com/traefik/traefik/tree/v2.11.33) (2025-12-17)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v2.11.32...v2.11.33)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[server]** Print access logs for rejected requests and warn about new behavior ([#12426](https://github.com/traefik/traefik/pull/12426) by [rtribotte](https://github.com/rtribotte))
|
||||
|
||||
**Documentation:**
|
||||
- Clarify doc about encoded characters rejection ([#12391](https://github.com/traefik/traefik/pull/12391) by [rtribotte](https://github.com/rtribotte))
|
||||
- Fix encoded characters entryPoint option documentation ([#12384](https://github.com/traefik/traefik/pull/12384) by [rtribotte](https://github.com/rtribotte))
|
||||
- Fix encoded characters option documentation ([#12373](https://github.com/traefik/traefik/pull/12373) by [kevinpollet](https://github.com/kevinpollet))
|
||||
|
||||
## [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:**
|
||||
**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))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1.2
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk add --no-cache --no-progress ca-certificates tzdata
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ Complete documentation is available at https://traefik.io`,
|
||||
func runCmd(staticConfiguration *static.Configuration) error {
|
||||
configureLogging(staticConfiguration)
|
||||
|
||||
// Display warning to advertise for new behavior of rejecting encoded characters in the request path.
|
||||
// Deprecated: this has to be removed in the next minor/major version.
|
||||
log.WithoutContext().Warnf("Starting with v2.11.32, Traefik now rejects some encoded characters in the request path by default. " +
|
||||
"Refer to the documentation for more details: https://doc.traefik.io/traefik/v2.11/migration/v2/#encoded-characters-in-request-path")
|
||||
|
||||
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
|
||||
|
||||
if err := roundrobin.SetDefaultWeight(0); err != nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk --no-cache --no-progress add \
|
||||
build-base \
|
||||
|
||||
@@ -733,4 +733,7 @@ Here is the list of the encoded characters that are rejected by default, along w
|
||||
| `%3f` or `%3F` | `?` (question mark) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedQuestionMark` |
|
||||
| `%23` | `#` (hash) | `entryPoints.<name>.`<br/>`.http.encodedCharacters`<br/>`.allowEncodedHash` |
|
||||
|
||||
Note: This check is not done against query parameters,
|
||||
but only against the request path as defined in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3).
|
||||
|
||||
Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation for more details.
|
||||
|
||||
@@ -123,9 +123,6 @@ 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. (Default: ```false```)
|
||||
|
||||
|
||||
@@ -132,9 +132,6 @@ HTTP/3 configuration. (Default: ```false```)
|
||||
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
|
||||
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```)
|
||||
|
||||
|
||||
@@ -127,14 +127,15 @@ 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
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedBackSlash: true
|
||||
allowEncodedNullCharacter: true
|
||||
allowEncodedSemicolon: true
|
||||
allowEncodedPercent: true
|
||||
allowEncodedQuestionMark: true
|
||||
allowEncodedHash: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
@@ -160,7 +161,7 @@ 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]
|
||||
[entryPoints.name.http.encodedCharacters]
|
||||
allowEncodedSlash = true
|
||||
allowEncodedBackSlash = true
|
||||
allowEncodedNullCharacter = true
|
||||
@@ -184,13 +185,13 @@ They can be defined by using a file (YAML or TOML) or CLI arguments.
|
||||
--entryPoints.name.proxyProtocol.trustedIPs=127.0.0.1,192.168.0.1
|
||||
--entryPoints.name.forwardedHeaders.insecure=true
|
||||
--entryPoints.name.forwardedHeaders.trustedIPs=127.0.0.1,192.168.0.1
|
||||
--entryPoints.name.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
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedSlash=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedBackSlash=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedNullCharacter=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedSemicolon=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedPercent=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedQuestionMark=true
|
||||
--entryPoints.name.http.encodedCharacters.allowEncodedHash=true
|
||||
```
|
||||
|
||||
### Address
|
||||
@@ -478,232 +479,6 @@ 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`
|
||||
@@ -1243,6 +1018,244 @@ entryPoints:
|
||||
| false | foo=bar&baz=bar;foo | foo=bar&baz=bar&foo |
|
||||
| true | foo=bar&baz=bar;foo | foo=bar&baz=bar%3Bfoo |
|
||||
|
||||
### Encoded Characters
|
||||
|
||||
You can configure Traefik to control the handling of encoded characters in request paths for security purposes.
|
||||
By default, Traefik rejects requests with path containing certain encoded characters that could be used in path traversal or other security attacks.
|
||||
|
||||
!!! info
|
||||
|
||||
This check is not done against the request query parameters,
|
||||
but only against the request path as defined in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3).
|
||||
|
||||
!!! warning "Security Considerations"
|
||||
|
||||
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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedSlash = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedBackSlash: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedBackSlash = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedNullCharacter: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedNullCharacter = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSemicolon: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedSemicolon = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedPercent: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedPercent = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedQuestionMark: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedQuestionMark = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.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"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedHash: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.http.encodedCharacters]
|
||||
allowEncodedHash = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.web.address=:80
|
||||
--entryPoints.web.http.encodedCharacters.allowEncodedHash=true
|
||||
```
|
||||
|
||||
### SanitizePath
|
||||
|
||||
_Optional, Default=true_
|
||||
|
||||
@@ -93,21 +93,22 @@ All encoded character filtering is enabled by default (`false` means encoded cha
|
||||
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)
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: false # %2F - Default: false (RECOMMENDED)
|
||||
allowEncodedBackSlash: false # %5C - Default: false (RECOMMENDED)
|
||||
allowEncodedNullCharacter: false # %00 - Default: false (RECOMMENDED)
|
||||
allowEncodedSemicolon: false # %3B - Default: false (RECOMMENDED)
|
||||
allowEncodedPercent: false # %25 - Default: false (RECOMMENDED)
|
||||
allowEncodedQuestionMark: false # %3F - Default: false (RECOMMENDED)
|
||||
allowEncodedHash: false # %23 - Default: false (RECOMMENDED)
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
[entryPoints.websecure]
|
||||
address = ":443"
|
||||
|
||||
[entryPoints.websecure.encodedCharacters]
|
||||
[entryPoints.websecure.http.encodedCharacters]
|
||||
allowEncodedSlash = false
|
||||
allowEncodedBackSlash = false
|
||||
allowEncodedNullCharacter = false
|
||||
@@ -119,11 +120,11 @@ entryPoints:
|
||||
|
||||
```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
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedSlash=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedBackSlash=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedNullCharacter=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedSemicolon=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedPercent=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedQuestionMark=false
|
||||
--entryPoints.websecure.http.encodedCharacters.allowEncodedHash=false
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
|
||||
ENV PATH="${PATH}:/venv/bin"
|
||||
|
||||
|
||||
20
integration/fixtures/simple_deny.toml
Normal file
20
integration/fixtures/simple_deny.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[api]
|
||||
insecure = true
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":8000"
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
[http.routers]
|
||||
[http.routers.router]
|
||||
service = "noop@internal"
|
||||
rule = "Host(`deny.localhost`)"
|
||||
34
integration/fixtures/simple_encoded_chars.toml
Normal file
34
integration/fixtures/simple_encoded_chars.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.strict]
|
||||
address = ":8000"
|
||||
# Default: no encoded characters allowed
|
||||
|
||||
[entryPoints.permissive]
|
||||
address = ":8001"
|
||||
[entryPoints.permissive.http.encodedCharacters]
|
||||
allowEncodedSlash = true
|
||||
|
||||
[api]
|
||||
insecure = true
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
[http.routers]
|
||||
[http.routers.sameRouter]
|
||||
service = "service1"
|
||||
rule = "Host(`test.localhost`)"
|
||||
|
||||
[http.services]
|
||||
[http.services.service1.loadBalancer]
|
||||
[[http.services.service1.loadBalancer.servers]]
|
||||
url = "{{ .Server1 }}"
|
||||
@@ -1369,16 +1369,15 @@ func (s *SimpleSuite) TestDenyFragment() {
|
||||
s.composeUp()
|
||||
defer s.composeDown()
|
||||
|
||||
s.traefikCmd(withConfigFile("fixtures/simple_default.toml"))
|
||||
s.traefikCmd(withConfigFile(s.adaptFile("fixtures/simple_deny.toml", struct{}{})))
|
||||
|
||||
// Expected a 404 as we did not configure anything
|
||||
err := try.GetRequest("http://127.0.0.1:8000/", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`deny.localhost`)"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:8000")
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
_, err = conn.Write([]byte("GET /#/?bar=toto;boo=titi HTTP/1.1\nHost: other.localhost\n\n"))
|
||||
_, err = conn.Write([]byte("GET /#/?bar=toto;boo=titi HTTP/1.1\nHost: deny.localhost\n\n"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
@@ -1482,3 +1481,72 @@ func (s *SimpleSuite) TestSanitizePath() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodedCharactersDifferentEntryPoints verifies that router handler caching does not interfere with
|
||||
// per-entry-point encoded characters configuration.
|
||||
// The same router should behave differently on different entry points.
|
||||
func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() {
|
||||
s.createComposeProject("base")
|
||||
|
||||
s.composeUp()
|
||||
defer s.composeDown()
|
||||
|
||||
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
|
||||
|
||||
file := s.adaptFile("fixtures/simple_encoded_chars.toml", struct {
|
||||
Server1 string
|
||||
}{whoami1URL})
|
||||
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`test.localhost`)"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
request string
|
||||
target string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "Encoded slash should be REJECTED on strict entry point",
|
||||
request: "GET /path%2Fwith%2Fslash HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
target: "127.0.0.1:8000", // strict entry point
|
||||
expected: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "Encoded slash should be ALLOWED on permissive entry point",
|
||||
request: "GET /path%2Fwith%2Fslash HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
target: "127.0.0.1:8001", // permissive entry point
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "Regular path should work on strict entry point",
|
||||
request: "GET /regular/path HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
target: "127.0.0.1:8000",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "Regular path should work on permissive entry point",
|
||||
request: "GET /regular/path HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
target: "127.0.0.1:8001",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
conn, err := net.Dial("tcp", test.target)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
_, err = conn.Write([]byte(test.request))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
assert.Equalf(s.T(), test.expected, resp.StatusCode, "%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
|
||||
|
||||
err = conn.Close()
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
}
|
||||
|
||||
4
pkg/api/testdata/entrypoint-bar.json
vendored
4
pkg/api/testdata/entrypoint-bar.json
vendored
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"address": ":81",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "bar"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"address": ":81",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "foo / bar"
|
||||
}
|
||||
|
||||
20
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
20
pkg/api/testdata/entrypoints-many-lastpage.json
vendored
@@ -1,37 +1,27 @@
|
||||
[
|
||||
{
|
||||
"address": ":14",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "ep14"
|
||||
},
|
||||
{
|
||||
"address": ":15",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "ep15"
|
||||
},
|
||||
{
|
||||
"address": ":16",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "ep16"
|
||||
},
|
||||
{
|
||||
"address": ":17",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "ep17"
|
||||
},
|
||||
{
|
||||
"address": ":18",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "ep18"
|
||||
}
|
||||
]
|
||||
|
||||
4
pkg/api/testdata/entrypoints-page2.json
vendored
4
pkg/api/testdata/entrypoints-page2.json
vendored
@@ -1,9 +1,7 @@
|
||||
[
|
||||
{
|
||||
"address": ":82",
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "web2"
|
||||
}
|
||||
]
|
||||
|
||||
8
pkg/api/testdata/entrypoints.json
vendored
8
pkg/api/testdata/entrypoints.json
vendored
@@ -8,9 +8,7 @@
|
||||
"192.168.1.4"
|
||||
]
|
||||
},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "web",
|
||||
"proxyProtocol": {
|
||||
"insecure": true,
|
||||
@@ -40,9 +38,7 @@
|
||||
"192.168.1.40"
|
||||
]
|
||||
},
|
||||
"http": {
|
||||
"encodedCharacters": {}
|
||||
},
|
||||
"http": {},
|
||||
"name": "websecure",
|
||||
"proxyProtocol": {
|
||||
"insecure": true,
|
||||
|
||||
@@ -24,8 +24,9 @@ type HTTPConfiguration struct {
|
||||
|
||||
// Model is a set of default router's values.
|
||||
type Model struct {
|
||||
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
|
||||
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
|
||||
DeniedEncodedPathCharacters *RouterDeniedEncodedPathCharacters `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
@@ -42,13 +43,60 @@ type Service struct {
|
||||
|
||||
// Router holds the router configuration.
|
||||
type Router struct {
|
||||
EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
|
||||
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
|
||||
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
|
||||
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
|
||||
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
|
||||
DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
|
||||
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
|
||||
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
|
||||
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
|
||||
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
|
||||
DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
DeniedEncodedPathCharacters RouterDeniedEncodedPathCharacters `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// RouterDeniedEncodedPathCharacters configures which encoded characters are allowed in the request path.
|
||||
type RouterDeniedEncodedPathCharacters struct {
|
||||
AllowEncodedSlash bool `description:"Defines whether requests with encoded slash characters in the path are allowed." json:"allowEncodedSlash,omitempty" toml:"allowEncodedSlash,omitempty" yaml:"allowEncodedSlash,omitempty" export:"true"`
|
||||
AllowEncodedBackSlash bool `description:"Defines whether requests with encoded back slash characters in the path are allowed." json:"allowEncodedBackSlash,omitempty" toml:"allowEncodedBackSlash,omitempty" yaml:"allowEncodedBackSlash,omitempty" export:"true"`
|
||||
AllowEncodedNullCharacter bool `description:"Defines whether requests with encoded null characters in the path are allowed." json:"allowEncodedNullCharacter,omitempty" toml:"allowEncodedNullCharacter,omitempty" yaml:"allowEncodedNullCharacter,omitempty" export:"true"`
|
||||
AllowEncodedSemicolon bool `description:"Defines whether requests with encoded semicolon characters in the path are allowed." json:"allowEncodedSemicolon,omitempty" toml:"allowEncodedSemicolon,omitempty" yaml:"allowEncodedSemicolon,omitempty" export:"true"`
|
||||
AllowEncodedPercent bool `description:"Defines whether requests with encoded percent characters in the path are allowed." json:"allowEncodedPercent,omitempty" toml:"allowEncodedPercent,omitempty" yaml:"allowEncodedPercent,omitempty" export:"true"`
|
||||
AllowEncodedQuestionMark bool `description:"Defines whether requests with encoded question mark characters in the path are allowed." json:"allowEncodedQuestionMark,omitempty" toml:"allowEncodedQuestionMark,omitempty" yaml:"allowEncodedQuestionMark,omitempty" export:"true"`
|
||||
AllowEncodedHash bool `description:"Defines whether requests with encoded hash characters in the path are allowed." json:"allowEncodedHash,omitempty" toml:"allowEncodedHash,omitempty" yaml:"allowEncodedHash,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// Map returns a map of unallowed encoded characters.
|
||||
func (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} {
|
||||
characters := make(map[string]struct{})
|
||||
|
||||
if !r.AllowEncodedSlash {
|
||||
characters["%2F"] = struct{}{}
|
||||
characters["%2f"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedBackSlash {
|
||||
characters["%5C"] = struct{}{}
|
||||
characters["%5c"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedNullCharacter {
|
||||
characters["%00"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedSemicolon {
|
||||
characters["%3B"] = struct{}{}
|
||||
characters["%3b"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedPercent {
|
||||
characters["%25"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedQuestionMark {
|
||||
characters["%3F"] = struct{}{}
|
||||
characters["%3f"] = struct{}{}
|
||||
}
|
||||
if !r.AllowEncodedHash {
|
||||
characters["%23"] = struct{}{}
|
||||
}
|
||||
|
||||
return characters
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
165
pkg/config/dynamic/http_config_test.go
Normal file
165
pkg/config/dynamic/http_config_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package dynamic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodedCharactersMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config RouterDeniedEncodedPathCharacters
|
||||
expected map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "Handles empty configuration",
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded slash when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded backslash when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedBackSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded null character when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedNullCharacter: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded semicolon when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedSemicolon: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded percent when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedPercent: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded question mark when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedQuestionMark: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded hash when allowed",
|
||||
config: RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedHash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := test.config.Map()
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -845,6 +845,11 @@ func (in *Model) DeepCopyInto(out *Model) {
|
||||
*out = new(RouterTLSConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.DeniedEncodedPathCharacters != nil {
|
||||
in, out := &in.DeniedEncodedPathCharacters, &out.DeniedEncodedPathCharacters
|
||||
*out = new(RouterDeniedEncodedPathCharacters)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1030,6 +1035,7 @@ func (in *Router) DeepCopyInto(out *Router) {
|
||||
*out = new(RouterTLSConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
out.DeniedEncodedPathCharacters = in.DeniedEncodedPathCharacters
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1043,6 +1049,22 @@ func (in *Router) DeepCopy() *Router {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RouterDeniedEncodedPathCharacters) DeepCopyInto(out *RouterDeniedEncodedPathCharacters) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouterDeniedEncodedPathCharacters.
|
||||
func (in *RouterDeniedEncodedPathCharacters) DeepCopy() *RouterDeniedEncodedPathCharacters {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RouterDeniedEncodedPathCharacters)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RouterTCPTLSConfig) DeepCopyInto(out *RouterTCPTLSConfig) {
|
||||
*out = *in
|
||||
|
||||
@@ -59,12 +59,12 @@ func (ep *EntryPoint) SetDefaults() {
|
||||
|
||||
// HTTPConfig is the HTTP configuration of an entry point.
|
||||
type HTTPConfig struct {
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
EncodedCharacters EncodedCharacters `description:"Defines which encoded characters are allowed in the request path." json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,omitempty" export:"true"`
|
||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty" export:"true"`
|
||||
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
EncodedCharacters *EncodedCharacters `description:"Defines which encoded characters are allowed in the request path." json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,omitempty" export:"true"`
|
||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty" export:"true"`
|
||||
SanitizePath *bool `description:"Defines whether to enable request path sanitization (removal of /./, /../ and multiple slash sequences)." json:"sanitizePath,omitempty" toml:"sanitizePath,omitempty" yaml:"sanitizePath,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
@@ -84,39 +84,6 @@ type EncodedCharacters struct {
|
||||
AllowEncodedHash bool `description:"Defines whether requests with encoded hash characters in the path are allowed." json:"allowEncodedHash,omitempty" toml:"allowEncodedHash,omitempty" yaml:"allowEncodedHash,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// Map returns a map of unallowed encoded characters.
|
||||
func (h *EncodedCharacters) Map() map[string]struct{} {
|
||||
characters := make(map[string]struct{})
|
||||
|
||||
if !h.AllowEncodedSlash {
|
||||
characters["%2F"] = struct{}{}
|
||||
characters["%2f"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedBackSlash {
|
||||
characters["%5C"] = struct{}{}
|
||||
characters["%5c"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedNullCharacter {
|
||||
characters["%00"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedSemicolon {
|
||||
characters["%3B"] = struct{}{}
|
||||
characters["%3b"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedPercent {
|
||||
characters["%25"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedQuestionMark {
|
||||
characters["%3F"] = struct{}{}
|
||||
characters["%3f"] = struct{}{}
|
||||
}
|
||||
if !h.AllowEncodedHash {
|
||||
characters["%23"] = struct{}{}
|
||||
}
|
||||
|
||||
return characters
|
||||
}
|
||||
|
||||
// HTTP2Config is the HTTP2 configuration of an entry point.
|
||||
type HTTP2Config struct {
|
||||
MaxConcurrentStreams int32 `description:"Specifies the number of concurrent streams per connection that each client is allowed to initiate." json:"maxConcurrentStreams,omitempty" toml:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty" export:"true"`
|
||||
|
||||
@@ -65,161 +65,3 @@ func TestEntryPointProtocol(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodedCharactersMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config EncodedCharacters
|
||||
expected map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "Handles empty configuration",
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded slash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded backslash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedBackSlash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Exclude encoded null character when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedNullCharacter: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded semicolon when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedSemicolon: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded percent when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedPercent: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded question mark when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedQuestionMark: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%23": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Exclude encoded hash when allowed",
|
||||
config: EncodedCharacters{
|
||||
AllowEncodedHash: true,
|
||||
},
|
||||
expected: map[string]struct{}{
|
||||
"%2F": {},
|
||||
"%2f": {},
|
||||
"%5C": {},
|
||||
"%5c": {},
|
||||
"%00": {},
|
||||
"%3B": {},
|
||||
"%3b": {},
|
||||
"%25": {},
|
||||
"%3F": {},
|
||||
"%3f": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := test.config.Map()
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str
|
||||
|
||||
func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
|
||||
for name, ep := range i.staticCfg.EntryPoints {
|
||||
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil {
|
||||
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && ep.HTTP.EncodedCharacters == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -220,6 +220,18 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
|
||||
Middlewares: ep.HTTP.Middlewares,
|
||||
}
|
||||
|
||||
if ep.HTTP.EncodedCharacters != nil {
|
||||
m.DeniedEncodedPathCharacters = &dynamic.RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedSlash: ep.HTTP.EncodedCharacters.AllowEncodedSlash,
|
||||
AllowEncodedBackSlash: ep.HTTP.EncodedCharacters.AllowEncodedBackSlash,
|
||||
AllowEncodedPercent: ep.HTTP.EncodedCharacters.AllowEncodedPercent,
|
||||
AllowEncodedQuestionMark: ep.HTTP.EncodedCharacters.AllowEncodedQuestionMark,
|
||||
AllowEncodedSemicolon: ep.HTTP.EncodedCharacters.AllowEncodedSemicolon,
|
||||
AllowEncodedHash: ep.HTTP.EncodedCharacters.AllowEncodedHash,
|
||||
AllowEncodedNullCharacter: ep.HTTP.EncodedCharacters.AllowEncodedNullCharacter,
|
||||
}
|
||||
}
|
||||
|
||||
if ep.HTTP.TLS != nil {
|
||||
m.TLS = &dynamic.RouterTLSConfig{
|
||||
Options: ep.HTTP.TLS.Options,
|
||||
|
||||
@@ -70,8 +70,7 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedCharacters": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -164,6 +164,12 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
|
||||
cp.Middlewares = append(m.Middlewares, cp.Middlewares...)
|
||||
|
||||
if m.DeniedEncodedPathCharacters != nil {
|
||||
// As the denied encoded path characters option is not configurable at the router level,
|
||||
// we can simply copy the whole structure to override the router's default config.
|
||||
cp.DeniedEncodedPathCharacters = *m.DeniedEncodedPathCharacters
|
||||
}
|
||||
|
||||
rtName := name
|
||||
if len(eps) > 1 {
|
||||
rtName = epName + "-" + name
|
||||
|
||||
62
pkg/server/router/deny.go
Normal file
62
pkg/server/router/deny.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
)
|
||||
|
||||
// denyFragment rejects the request if the URL path contains a fragment (hash character).
|
||||
// When go receives an HTTP request, it assumes the absence of fragment URL.
|
||||
// However, it is still possible to send a fragment in the request.
|
||||
// In this case, Traefik will encode the '#' character, altering the request's intended meaning.
|
||||
// To avoid this behavior, the following function rejects requests that include a fragment in the URL.
|
||||
func denyFragment(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if strings.Contains(req.URL.RawPath, "#") {
|
||||
log.WithoutContext().Debugf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
// denyEncodedPathCharacters reject the request if the escaped path contains encoded characters in the given list.
|
||||
func denyEncodedPathCharacters(encodedCharacters map[string]struct{}, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if len(encodedCharacters) == 0 {
|
||||
h.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
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.WithoutContext().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)
|
||||
})
|
||||
}
|
||||
98
pkg/server/router/deny_test.go
Normal file
98
pkg/server/router/deny_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_denyFragment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "Rejects fragment character",
|
||||
url: "http://example.com/#",
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Allows without fragment",
|
||||
url: "http://example.com/",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := denyFragment(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(res, req)
|
||||
|
||||
assert.Equal(t, test.wantStatus, res.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_denyEncodedPathCharacters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
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 := denyEncodedPathCharacters(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,13 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager.
|
||||
func NewManager(conf *runtime.Configuration, serviceManager serviceManager, middlewaresBuilder middlewareBuilder, chainBuilder *middleware.ChainBuilder, metricsRegistry metrics.Registry, tlsManager *tls.Manager) *Manager {
|
||||
func NewManager(conf *runtime.Configuration,
|
||||
serviceManager serviceManager,
|
||||
middlewaresBuilder middlewareBuilder,
|
||||
chainBuilder *middleware.ChainBuilder,
|
||||
metricsRegistry metrics.Registry,
|
||||
tlsManager *tls.Manager,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
routerHandlers: make(map[string]http.Handler),
|
||||
serviceManager: serviceManager,
|
||||
@@ -217,6 +223,15 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
|
||||
chain = chain.Append(denyrouterrecursion.WrapHandler(routerName))
|
||||
}
|
||||
|
||||
// Here we are adding deny handlers for encoded path characters and fragment.
|
||||
// Deny handler are only added for root routers, child routers are protected by their parent router deny handlers.
|
||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||
return denyFragment(next), nil
|
||||
})
|
||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||
return denyEncodedPathCharacters(router.DeniedEncodedPathCharacters.Map(), next), nil
|
||||
})
|
||||
|
||||
return chain.Extend(*mHandler).Append(tHandler).Then(sHandler)
|
||||
}
|
||||
|
||||
|
||||
@@ -901,6 +901,122 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
||||
assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
|
||||
}
|
||||
|
||||
func TestManager_BuildHandlers_Deny(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers map[string]*dynamic.Router
|
||||
services map[string]*dynamic.Service
|
||||
requestPath string
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
desc: "unallowed request with encoded slash",
|
||||
requestPath: "/foo%2F",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/`)",
|
||||
Service: "service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "allowed request with encoded slash",
|
||||
requestPath: "/foo%2F",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/`)",
|
||||
Service: "service",
|
||||
DeniedEncodedPathCharacters: dynamic.RouterDeniedEncodedPathCharacters{
|
||||
AllowEncodedSlash: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatusCode: http.StatusBadGateway,
|
||||
},
|
||||
{
|
||||
desc: "unallowed request with fragment",
|
||||
requestPath: "/foo#",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/`)",
|
||||
Service: "service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Create runtime routers
|
||||
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||
for name, router := range test.routers {
|
||||
runtimeRouters[name] = &runtime.RouterInfo{
|
||||
Router: router,
|
||||
}
|
||||
}
|
||||
|
||||
// Create runtime services
|
||||
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
||||
for name, service := range test.services {
|
||||
runtimeServices[name] = &runtime.ServiceInfo{
|
||||
Service: service,
|
||||
}
|
||||
}
|
||||
|
||||
conf := &runtime.Configuration{
|
||||
Routers: runtimeRouters,
|
||||
Services: runtimeServices,
|
||||
}
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
serviceManager := service.NewManager(conf.Services, nil, nil, roundTripperManager)
|
||||
middlewaresBuilder := middleware.NewBuilder(conf.Middlewares, serviceManager, nil)
|
||||
chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
|
||||
routerManager := NewManager(conf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager)
|
||||
|
||||
// Build handlers
|
||||
ctx := t.Context()
|
||||
handlers := routerManager.BuildHandlers(ctx, []string{"web"}, false)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, test.requestPath, http.NoBody)
|
||||
|
||||
handlers["web"].ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, test.expectedStatusCode, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type staticRoundTripperGetter struct {
|
||||
res *http.Response
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
|
||||
// RouterFactory the factory of TCP/UDP routers.
|
||||
type RouterFactory struct {
|
||||
entryPointsTCP []string
|
||||
entryPointsUDP []string
|
||||
entryPointsTCP []string
|
||||
entryPointsUDP []string
|
||||
|
||||
allowACMEByPass map[string]bool
|
||||
|
||||
managerFactory *service.ManagerFactory
|
||||
|
||||
@@ -606,10 +606,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
||||
|
||||
handler = normalizePath(handler)
|
||||
|
||||
handler = denyFragment(handler)
|
||||
|
||||
handler = denyEncodedCharacters(configuration.HTTP.EncodedCharacters.Map(), handler)
|
||||
|
||||
serverHTTP := &http.Server{
|
||||
Protocols: &protocols,
|
||||
Handler: handler,
|
||||
@@ -709,54 +705,6 @@ 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.
|
||||
// To avoid this behavior, the following function rejects requests that include a fragment in the URL.
|
||||
func denyFragment(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if strings.Contains(req.URL.RawPath, "#") {
|
||||
log.WithoutContext().Debugf("Rejecting request because it contains a fragment in the URL path: %s", req.URL.RawPath)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
// sanitizePath removes the "..", "." and duplicate slash segments from the URL according to https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.3.
|
||||
// It cleans the request URL Path and RawPath, and updates the request URI.
|
||||
func sanitizePath(h http.Handler) http.Handler {
|
||||
|
||||
@@ -429,59 +429,6 @@ 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{
|
||||
@@ -629,8 +576,10 @@ func TestPathOperations(t *testing.T) {
|
||||
configuration.SetDefaults()
|
||||
|
||||
// We need to allow some of the suspicious encoded characters to test the path operations in case they are authorized.
|
||||
configuration.HTTP.EncodedCharacters.AllowEncodedSlash = true
|
||||
configuration.HTTP.EncodedCharacters.AllowEncodedPercent = true
|
||||
configuration.HTTP.EncodedCharacters = &static.EncodedCharacters{
|
||||
AllowEncodedSlash: true,
|
||||
AllowEncodedPercent: true,
|
||||
}
|
||||
|
||||
// Create the HTTP server using createHTTPServer.
|
||||
server, err := createHTTPServer(t.Context(), ln, configuration, false, requestdecorator.New(nil))
|
||||
|
||||
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
|
||||
OutputType = "file"
|
||||
FileName = "traefik_changelog.md"
|
||||
|
||||
# example new bugfix v2.11.32
|
||||
# example new bugfix v2.11.33
|
||||
CurrentRef = "v2.11"
|
||||
PreviousRef = "v2.11.31"
|
||||
PreviousRef = "v2.11.32"
|
||||
BaseBranch = "v2.11"
|
||||
FutureCurrentRefName = "v2.11.32"
|
||||
FutureCurrentRefName = "v2.11.33"
|
||||
|
||||
ThresholdPreviousRef = 10000
|
||||
ThresholdCurrentRef = 10000
|
||||
|
||||
@@ -1 +1 @@
|
||||
20.11.0
|
||||
24.11.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.9-alpine3.20
|
||||
FROM node:24-alpine3.23
|
||||
# Current Active LTS release according to (https://nodejs.org/en/about/releases/)
|
||||
|
||||
ENV WEBUI_DIR=/src/webui
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"cookie": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22 || ^20 || ^18 || ^16",
|
||||
"node": "^24 || ^22",
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.22.22"
|
||||
},
|
||||
|
||||
@@ -132,7 +132,7 @@ module.exports = configure(function (ctx) {
|
||||
},
|
||||
target: {
|
||||
browser: ['edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||
node: 'node20'
|
||||
node: 'node24'
|
||||
},
|
||||
publicPath: process.env.APP_PUBLIC_PATH || '',
|
||||
env: process.env.APP_ENV === 'development'
|
||||
|
||||
Reference in New Issue
Block a user