1
0
mirror of https://github.com/containous/traefik.git synced 2025-12-27 00:23:53 +03:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Romain
90ce858347 Fix deny encoded characters
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2025-12-23 16:00:05 +01:00
Jesper Noordsij
8ebab1b243 Update Dockerfiles to Alpine 3.23 2025-12-17 15:26:06 +01:00
Romain
5fe10e2098 Prepare release v2.11.33 2025-12-17 10:56:04 +01:00
Jesper Noordsij
fd36de5a0a Bump webui to NodeJS v24 2025-12-17 10:36:05 +01:00
Romain
60b19b7b81 Print access logs for rejected requests and warn about new behavior 2025-12-16 16:20:05 +01:00
Romain
e0e49533ab Clarify doc about encodedCharacters rejection 2025-12-09 15:28:04 +01:00
Romain
351dcbd186 Fix encodedCharacters entryPoint option documentation 2025-12-08 10:44:04 +01:00
Kevin Pollet
7f40f3cd58 Fix encoded characters option documentation 2025-12-05 15:06:04 +01:00
38 changed files with 1007 additions and 628 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22
FROM alpine:3.23
RUN apk --no-cache --no-progress add \
build-base \

View File

@@ -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.

View File

@@ -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```)

View File

@@ -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```)

View File

@@ -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_

View File

@@ -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
```

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22
FROM alpine:3.23
ENV PATH="${PATH}:/venv/bin"

View 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`)"

View 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 }}"

View File

@@ -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)
}
}

View File

@@ -1,7 +1,5 @@
{
"address": ":81",
"http": {
"encodedCharacters": {}
},
"http": {},
"name": "bar"
}

View File

@@ -1,7 +1,5 @@
{
"address": ":81",
"http": {
"encodedCharacters": {}
},
"http": {},
"name": "foo / bar"
}

View File

@@ -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"
}
]

View File

@@ -1,9 +1,7 @@
[
{
"address": ":82",
"http": {
"encodedCharacters": {}
},
"http": {},
"name": "web2"
}
]

View File

@@ -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,

View File

@@ -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

View 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)
})
}
}

View File

@@ -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

View File

@@ -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"`

View File

@@ -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)
})
}
}

View File

@@ -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,

View File

@@ -70,8 +70,7 @@
]
}
]
},
"encodedCharacters": {}
}
}
}
},

View File

@@ -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
View 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)
})
}

View 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)
})
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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

View File

@@ -1 +1 @@
20.11.0
24.11.0

View File

@@ -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

View File

@@ -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"
},

View File

@@ -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'