Add encodedCharacters middleware

This commit is contained in:
Gina A.
2026-01-21 10:24:12 +01:00
committed by GitHub
parent 954eaab5f7
commit 94eba471f1
18 changed files with 621 additions and 75 deletions
@@ -1105,6 +1105,39 @@ spec:
containing user credentials.
type: string
type: object
encodedCharacters:
description: EncodedCharacters configures which encoded characters
are allowed in the request path.
properties:
allowEncodedBackSlash:
description: AllowEncodedBackSlash defines whether requests with
encoded back slash characters in the path are allowed.
type: boolean
allowEncodedHash:
description: AllowEncodedHash defines whether requests with encoded
hash characters in the path are allowed.
type: boolean
allowEncodedNullCharacter:
description: AllowEncodedNullCharacter defines whether requests
with encoded null characters in the path are allowed.
type: boolean
allowEncodedPercent:
description: AllowEncodedPercent defines whether requests with
encoded percent characters in the path are allowed.
type: boolean
allowEncodedQuestionMark:
description: AllowEncodedQuestionMark defines whether requests
with encoded question mark characters in the path are allowed.
type: boolean
allowEncodedSemicolon:
description: AllowEncodedSemicolon defines whether requests with
encoded semicolon characters in the path are allowed.
type: boolean
allowEncodedSlash:
description: AllowEncodedSlash defines whether requests with encoded
slash characters in the path are allowed.
type: boolean
type: object
errors:
description: |-
ErrorPage holds the custom error middleware configuration.
@@ -260,6 +260,39 @@ spec:
containing user credentials.
type: string
type: object
encodedCharacters:
description: EncodedCharacters configures which encoded characters
are allowed in the request path.
properties:
allowEncodedBackSlash:
description: AllowEncodedBackSlash defines whether requests with
encoded back slash characters in the path are allowed.
type: boolean
allowEncodedHash:
description: AllowEncodedHash defines whether requests with encoded
hash characters in the path are allowed.
type: boolean
allowEncodedNullCharacter:
description: AllowEncodedNullCharacter defines whether requests
with encoded null characters in the path are allowed.
type: boolean
allowEncodedPercent:
description: AllowEncodedPercent defines whether requests with
encoded percent characters in the path are allowed.
type: boolean
allowEncodedQuestionMark:
description: AllowEncodedQuestionMark defines whether requests
with encoded question mark characters in the path are allowed.
type: boolean
allowEncodedSemicolon:
description: AllowEncodedSemicolon defines whether requests with
encoded semicolon characters in the path are allowed.
type: boolean
allowEncodedSlash:
description: AllowEncodedSlash defines whether requests with encoded
slash characters in the path are allowed.
type: boolean
type: object
errors:
description: |-
ErrorPage holds the custom error middleware configuration.
@@ -0,0 +1,62 @@
---
title: "Traefik EncodedCharacters Documentation"
description: "In Traefik Proxy, the EncodedCharacters middleware controls which ambiguous reserved encoded characters are allowed in the request path. Read the technical documentation."
---
The EncodedCharacters middleware controls which ambiguous reserved encoded characters are allowed in the request path.
When you use this middleware, by default, potentially dangerous encoded characters are rejected for security enhancement.
## Configuration Examples
```yaml tab="Docker & Swarm"
# Allow encoded slash in the request path.
labels:
- "traefik.http.middlewares.test-encodedchars.encodedcharacters.allowencodedslash=true"
```
```yaml tab="Kubernetes"
# Allow encoded slash in the request path.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-encodedchars
spec:
encodedCharacters:
allowEncodedSlash: true
```
```yaml tab="Consul Catalog"
# Allow encoded slash in the request path.
- "traefik.http.middlewares.test-encodedchars.encodedcharacters.allowencodedslash=true"
```
```yaml tab="File (YAML)"
# Allow encoded slash in the request path.
http:
middlewares:
test-encodedchars:
encodedCharacters:
allowEncodedSlash: true
```
```toml tab="File (TOML)"
# Allow encoded slash in the request path.
[http.middlewares]
[http.middlewares.test-encodedchars.encodedCharacters]
allowEncodedSlash = true
```
## Configuration Options
When you are configuring these options, check if your backend is fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
This helps avoid split-view situation, where Traefik and your backend interpret the same URL differently.
| Field | Description | Default | Required |
|-------------------------|--------------------------------------------------------------------|---------| -------- |
| <a id="opt-allowEncodedSlash" href="#opt-allowEncodedSlash" title="#opt-allowEncodedSlash">`allowEncodedSlash`</a> | Allow encoded slash (`%2F` and `%2f`) in the request path. | `false` | No |
| <a id="opt-allowEncodedBackSlash" href="#opt-allowEncodedBackSlash" title="#opt-allowEncodedBackSlash">`allowEncodedBackSlash`</a> | Allow encoded backslash (`%5C` and `%5c`) in the request path. | `false` | No |
| <a id="opt-allowEncodedSemicolon" href="#opt-allowEncodedSemicolon" title="#opt-allowEncodedSemicolon">`allowEncodedSemicolon`</a> | Allow encoded semicolon (`%3B` and `%3b`) in the request path. | `false` | No |
| <a id="opt-allowEncodedPercent" href="#opt-allowEncodedPercent" title="#opt-allowEncodedPercent">`allowEncodedPercent`</a> | Allow encoded percent (`%25`) in the request path. | `false` | No |
| <a id="opt-allowEncodedQuestionMark" href="#opt-allowEncodedQuestionMark" title="#opt-allowEncodedQuestionMark">`allowEncodedQuestionMark`</a> | Allow encoded question mark (`%3F` and `%3f`) in the request path. | `false` | No |
| <a id="opt-allowEncodedHash" href="#opt-allowEncodedHash" title="#opt-allowEncodedHash">`allowEncodedHash`</a> | Allow encoded hash (`%23`) in the request path. | `false` | No |
@@ -18,8 +18,8 @@ Middlewares that use the same protocol can be combined into chains to fit every
## Available HTTP Middlewares
| Middleware | Purpose | Area |
|-------------------------------------------|---------------------------------------------------|-----------------------------|
| Middleware | Purpose | Area |
|------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|-----------------------------|
| <a id="opt-AddPrefix" href="#opt-AddPrefix" title="#opt-AddPrefix">[AddPrefix](addprefix.md)</a> | Adds a Path Prefix | Path Modifier |
| <a id="opt-BasicAuth" href="#opt-BasicAuth" title="#opt-BasicAuth">[BasicAuth](basicauth.md)</a> | Adds Basic Authentication | Security, Authentication |
| <a id="opt-Buffering" href="#opt-Buffering" title="#opt-Buffering">[Buffering](buffering.md)</a> | Buffers the request/response | Request Lifecycle |
@@ -28,6 +28,7 @@ Middlewares that use the same protocol can be combined into chains to fit every
| <a id="opt-Compress" href="#opt-Compress" title="#opt-Compress">[Compress](compress.md)</a> | Compresses the response | Content Modifier |
| <a id="opt-ContentType" href="#opt-ContentType" title="#opt-ContentType">[ContentType](contenttype.md)</a> | Handles Content-Type auto-detection | Misc |
| <a id="opt-DigestAuth" href="#opt-DigestAuth" title="#opt-DigestAuth">[DigestAuth](digestauth.md)</a> | Adds Digest Authentication | Security, Authentication |
| <a id="opt-EncodedCharacters" href="#opt-EncodedCharacters" title="#opt-EncodedCharacters">[EncodedCharacters](encodedcharacters.md)</a> | Defines allowed reserved encoded characters in the request path | Security, Request Lifecycle |
| <a id="opt-Errors" href="#opt-Errors" title="#opt-Errors">[Errors](errorpages.md)</a> | Defines custom error pages | Request Lifecycle |
| <a id="opt-ForwardAuth" href="#opt-ForwardAuth" title="#opt-ForwardAuth">[ForwardAuth](forwardauth.md)</a> | Delegates Authentication | Security, Authentication |
| <a id="opt-GrpcWeb" href="#opt-GrpcWeb" title="#opt-GrpcWeb">[GrpcWeb](grpcweb.md)</a> | Converts gRPC Web requests to HTTP/2 gRPC requests. | Request |
@@ -191,15 +191,24 @@
realm = "foobar"
headerField = "foobar"
[http.middlewares.Middleware09]
[http.middlewares.Middleware09.errors]
[http.middlewares.Middleware09.encodedCharacters]
allowEncodedSlash = true
allowEncodedBackSlash = true
allowEncodedNullCharacter = true
allowEncodedSemicolon = true
allowEncodedPercent = true
allowEncodedQuestionMark = true
allowEncodedHash = true
[http.middlewares.Middleware10]
[http.middlewares.Middleware10.errors]
status = ["foobar", "foobar"]
service = "foobar"
query = "foobar"
[http.middlewares.Middleware09.errors.statusRewrites]
[http.middlewares.Middleware10.errors.statusRewrites]
name0 = 42
name1 = 42
[http.middlewares.Middleware10]
[http.middlewares.Middleware10.forwardAuth]
[http.middlewares.Middleware11]
[http.middlewares.Middleware11.forwardAuth]
address = "foobar"
trustForwardHeader = true
authResponseHeaders = ["foobar", "foobar"]
@@ -211,17 +220,17 @@
maxBodySize = 42
preserveLocationHeader = true
preserveRequestMethod = true
[http.middlewares.Middleware10.forwardAuth.tls]
[http.middlewares.Middleware11.forwardAuth.tls]
ca = "foobar"
cert = "foobar"
key = "foobar"
insecureSkipVerify = true
caOptional = true
[http.middlewares.Middleware11]
[http.middlewares.Middleware11.grpcWeb]
allowOrigins = ["foobar", "foobar"]
[http.middlewares.Middleware12]
[http.middlewares.Middleware12.headers]
[http.middlewares.Middleware12.grpcWeb]
allowOrigins = ["foobar", "foobar"]
[http.middlewares.Middleware13]
[http.middlewares.Middleware13.headers]
accessControlAllowCredentials = true
accessControlAllowHeaders = ["foobar", "foobar"]
accessControlAllowMethods = ["foobar", "foobar"]
@@ -252,49 +261,49 @@
sslTemporaryRedirect = true
sslHost = "foobar"
sslForceHost = true
[http.middlewares.Middleware12.headers.customRequestHeaders]
[http.middlewares.Middleware13.headers.customRequestHeaders]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware12.headers.customResponseHeaders]
[http.middlewares.Middleware13.headers.customResponseHeaders]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware12.headers.sslProxyHeaders]
[http.middlewares.Middleware13.headers.sslProxyHeaders]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware13]
[http.middlewares.Middleware13.ipAllowList]
[http.middlewares.Middleware14]
[http.middlewares.Middleware14.ipAllowList]
sourceRange = ["foobar", "foobar"]
rejectStatusCode = 42
[http.middlewares.Middleware13.ipAllowList.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware14]
[http.middlewares.Middleware14.ipWhiteList]
sourceRange = ["foobar", "foobar"]
[http.middlewares.Middleware14.ipWhiteList.ipStrategy]
[http.middlewares.Middleware14.ipAllowList.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware15]
[http.middlewares.Middleware15.inFlightReq]
[http.middlewares.Middleware15.ipWhiteList]
sourceRange = ["foobar", "foobar"]
[http.middlewares.Middleware15.ipWhiteList.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware16]
[http.middlewares.Middleware16.inFlightReq]
amount = 42
[http.middlewares.Middleware15.inFlightReq.sourceCriterion]
[http.middlewares.Middleware16.inFlightReq.sourceCriterion]
requestHeaderName = "foobar"
requestHost = true
[http.middlewares.Middleware15.inFlightReq.sourceCriterion.ipStrategy]
[http.middlewares.Middleware16.inFlightReq.sourceCriterion.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware16]
[http.middlewares.Middleware16.passTLSClientCert]
[http.middlewares.Middleware17]
[http.middlewares.Middleware17.passTLSClientCert]
pem = true
[http.middlewares.Middleware16.passTLSClientCert.info]
[http.middlewares.Middleware17.passTLSClientCert.info]
notAfter = true
notBefore = true
sans = true
serialNumber = true
[http.middlewares.Middleware16.passTLSClientCert.info.subject]
[http.middlewares.Middleware17.passTLSClientCert.info.subject]
country = true
province = true
locality = true
@@ -303,7 +312,7 @@
commonName = true
serialNumber = true
domainComponent = true
[http.middlewares.Middleware16.passTLSClientCert.info.issuer]
[http.middlewares.Middleware17.passTLSClientCert.info.issuer]
country = true
province = true
locality = true
@@ -311,27 +320,27 @@
commonName = true
serialNumber = true
domainComponent = true
[http.middlewares.Middleware17]
[http.middlewares.Middleware17.plugin]
[http.middlewares.Middleware17.plugin.PluginConf0]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware17.plugin.PluginConf1]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware18]
[http.middlewares.Middleware18.rateLimit]
[http.middlewares.Middleware18.plugin]
[http.middlewares.Middleware18.plugin.PluginConf0]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware18.plugin.PluginConf1]
name0 = "foobar"
name1 = "foobar"
[http.middlewares.Middleware19]
[http.middlewares.Middleware19.rateLimit]
average = 42
period = "42s"
burst = 42
[http.middlewares.Middleware18.rateLimit.sourceCriterion]
[http.middlewares.Middleware19.rateLimit.sourceCriterion]
requestHeaderName = "foobar"
requestHost = true
[http.middlewares.Middleware18.rateLimit.sourceCriterion.ipStrategy]
[http.middlewares.Middleware19.rateLimit.sourceCriterion.ipStrategy]
depth = 42
excludedIPs = ["foobar", "foobar"]
ipv6Subnet = 42
[http.middlewares.Middleware18.rateLimit.redis]
[http.middlewares.Middleware19.rateLimit.redis]
endpoints = ["foobar", "foobar"]
username = "foobar"
password = "foobar"
@@ -342,38 +351,38 @@
readTimeout = "42s"
writeTimeout = "42s"
dialTimeout = "42s"
[http.middlewares.Middleware18.rateLimit.redis.tls]
[http.middlewares.Middleware19.rateLimit.redis.tls]
ca = "foobar"
cert = "foobar"
key = "foobar"
insecureSkipVerify = true
[http.middlewares.Middleware19]
[http.middlewares.Middleware19.redirectRegex]
[http.middlewares.Middleware20]
[http.middlewares.Middleware20.redirectRegex]
regex = "foobar"
replacement = "foobar"
permanent = true
[http.middlewares.Middleware20]
[http.middlewares.Middleware20.redirectScheme]
[http.middlewares.Middleware21]
[http.middlewares.Middleware21.redirectScheme]
scheme = "foobar"
port = "foobar"
permanent = true
[http.middlewares.Middleware21]
[http.middlewares.Middleware21.replacePath]
path = "foobar"
[http.middlewares.Middleware22]
[http.middlewares.Middleware22.replacePathRegex]
[http.middlewares.Middleware22.replacePath]
path = "foobar"
[http.middlewares.Middleware23]
[http.middlewares.Middleware23.replacePathRegex]
regex = "foobar"
replacement = "foobar"
[http.middlewares.Middleware23]
[http.middlewares.Middleware23.retry]
[http.middlewares.Middleware24]
[http.middlewares.Middleware24.retry]
attempts = 42
initialInterval = "42s"
[http.middlewares.Middleware24]
[http.middlewares.Middleware24.stripPrefix]
[http.middlewares.Middleware25]
[http.middlewares.Middleware25.stripPrefix]
prefixes = ["foobar", "foobar"]
forceSlash = true
[http.middlewares.Middleware25]
[http.middlewares.Middleware25.stripPrefixRegex]
[http.middlewares.Middleware26]
[http.middlewares.Middleware26.stripPrefixRegex]
regex = ["foobar", "foobar"]
[http.serversTransports]
[http.serversTransports.ServersTransport0]
@@ -205,6 +205,15 @@ http:
realm: foobar
headerField: foobar
Middleware09:
encodedCharacters:
allowEncodedSlash: true
allowEncodedBackSlash: true
allowEncodedNullCharacter: true
allowEncodedSemicolon: true
allowEncodedPercent: true
allowEncodedQuestionMark: true
allowEncodedHash: true
Middleware10:
errors:
status:
- foobar
@@ -214,7 +223,7 @@ http:
name1: 42
service: foobar
query: foobar
Middleware10:
Middleware11:
forwardAuth:
address: foobar
tls:
@@ -239,12 +248,12 @@ http:
maxBodySize: 42
preserveLocationHeader: true
preserveRequestMethod: true
Middleware11:
Middleware12:
grpcWeb:
allowOrigins:
- foobar
- foobar
Middleware12:
Middleware13:
headers:
customRequestHeaders:
name0: foobar
@@ -299,7 +308,7 @@ http:
sslTemporaryRedirect: true
sslHost: foobar
sslForceHost: true
Middleware13:
Middleware14:
ipAllowList:
sourceRange:
- foobar
@@ -311,7 +320,7 @@ http:
- foobar
ipv6Subnet: 42
rejectStatusCode: 42
Middleware14:
Middleware15:
ipWhiteList:
sourceRange:
- foobar
@@ -322,7 +331,7 @@ http:
- foobar
- foobar
ipv6Subnet: 42
Middleware15:
Middleware16:
inFlightReq:
amount: 42
sourceCriterion:
@@ -334,7 +343,7 @@ http:
ipv6Subnet: 42
requestHeaderName: foobar
requestHost: true
Middleware16:
Middleware17:
passTLSClientCert:
pem: true
info:
@@ -359,7 +368,7 @@ http:
commonName: true
serialNumber: true
domainComponent: true
Middleware17:
Middleware18:
plugin:
PluginConf0:
name0: foobar
@@ -367,7 +376,7 @@ http:
PluginConf1:
name0: foobar
name1: foobar
Middleware18:
Middleware19:
rateLimit:
average: 42
period: 42s
@@ -399,34 +408,34 @@ http:
readTimeout: 42s
writeTimeout: 42s
dialTimeout: 42s
Middleware19:
Middleware20:
redirectRegex:
regex: foobar
replacement: foobar
permanent: true
Middleware20:
Middleware21:
redirectScheme:
scheme: foobar
port: foobar
permanent: true
Middleware21:
Middleware22:
replacePath:
path: foobar
Middleware22:
Middleware23:
replacePathRegex:
regex: foobar
replacement: foobar
Middleware23:
Middleware24:
retry:
attempts: 42
initialInterval: 42s
Middleware24:
Middleware25:
stripPrefix:
prefixes:
- foobar
- foobar
forceSlash: true
Middleware25:
Middleware26:
stripPrefixRegex:
regex:
- foobar
+5
View File
@@ -133,3 +133,8 @@ entryPoints:
--entryPoints.websecure.http.encodedCharacters.allowEncodedQuestionMark=false
--entryPoints.websecure.http.encodedCharacters.allowEncodedHash=false
```
!!! info "Encoded Characters filtering on a per-route basis"
If you need to configure encoded character filtering on a per-route basis, you can use the `EncodedCharacters` middleware.
Refer to the documentation for the [`EncodedCharacter` middleware](../reference/routing-configuration/http/middlewares/encodedcharacters.md) for detailed implementation instructions and configuration options.
+1
View File
@@ -292,6 +292,7 @@ nav:
- 'ContentType': 'reference/routing-configuration/http/middlewares/contenttype.md'
- 'DigestAuth': 'reference/routing-configuration/http/middlewares/digestauth.md'
- '<span class="nav-link-with-icon">Distributed RateLimit <img src="https://doc.traefik.io/traefik-hub/img/ps-traefik-hub-logo-light.svg" class="menu-icon" alt="Traefik Hub API Gateway"></span>' : 'reference/routing-configuration/http/middlewares/distributed-ratelimit.md'
- 'EncodedCharacters': 'reference/routing-configuration/http/middlewares/encodedcharacters.md'
- 'Errors': 'reference/routing-configuration/http/middlewares/errorpages.md'
- 'ForwardAuth': 'reference/routing-configuration/http/middlewares/forwardauth.md'
- 'GrpcWeb': 'reference/routing-configuration/http/middlewares/grpcweb.md'
@@ -1106,6 +1106,39 @@ spec:
containing user credentials.
type: string
type: object
encodedCharacters:
description: EncodedCharacters configures which encoded characters
are allowed in the request path.
properties:
allowEncodedBackSlash:
description: AllowEncodedBackSlash defines whether requests with
encoded back slash characters in the path are allowed.
type: boolean
allowEncodedHash:
description: AllowEncodedHash defines whether requests with encoded
hash characters in the path are allowed.
type: boolean
allowEncodedNullCharacter:
description: AllowEncodedNullCharacter defines whether requests
with encoded null characters in the path are allowed.
type: boolean
allowEncodedPercent:
description: AllowEncodedPercent defines whether requests with
encoded percent characters in the path are allowed.
type: boolean
allowEncodedQuestionMark:
description: AllowEncodedQuestionMark defines whether requests
with encoded question mark characters in the path are allowed.
type: boolean
allowEncodedSemicolon:
description: AllowEncodedSemicolon defines whether requests with
encoded semicolon characters in the path are allowed.
type: boolean
allowEncodedSlash:
description: AllowEncodedSlash defines whether requests with encoded
slash characters in the path are allowed.
type: boolean
type: object
errors:
description: |-
ErrorPage holds the custom error middleware configuration.
+21
View File
@@ -27,6 +27,7 @@ type Middleware struct {
IPWhiteList *IPWhiteList `json:"ipWhiteList,omitempty" toml:"ipWhiteList,omitempty" yaml:"ipWhiteList,omitempty" export:"true"`
IPAllowList *IPAllowList `json:"ipAllowList,omitempty" toml:"ipAllowList,omitempty" yaml:"ipAllowList,omitempty" export:"true"`
Headers *Headers `json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"`
EncodedCharacters *EncodedCharacters `json:"encodedCharacters,omitempty" toml:"encodedCharacters,omitempty" yaml:"encodedCharacters,omitempty" export:"true"`
Errors *ErrorPage `json:"errors,omitempty" toml:"errors,omitempty" yaml:"errors,omitempty" export:"true"`
RateLimit *RateLimit `json:"rateLimit,omitempty" toml:"rateLimit,omitempty" yaml:"rateLimit,omitempty" export:"true"`
RedirectRegex *RedirectRegex `json:"redirectRegex,omitempty" toml:"redirectRegex,omitempty" yaml:"redirectRegex,omitempty" export:"true"`
@@ -217,6 +218,26 @@ type DigestAuth struct {
// +k8s:deepcopy-gen=true
// EncodedCharacters configures which encoded characters are allowed in the request path.
type EncodedCharacters struct {
// AllowEncodedSlash defines whether requests with encoded slash characters in the path are allowed.
AllowEncodedSlash bool `json:"allowEncodedSlash,omitempty" toml:"allowEncodedSlash,omitempty" yaml:"allowEncodedSlash,omitempty" export:"true"`
// AllowEncodedBackSlash defines whether requests with encoded back slash characters in the path are allowed.
AllowEncodedBackSlash bool `json:"allowEncodedBackSlash,omitempty" toml:"allowEncodedBackSlash,omitempty" yaml:"allowEncodedBackSlash,omitempty" export:"true"`
// AllowEncodedNullCharacter defines whether requests with encoded null characters in the path are allowed.
AllowEncodedNullCharacter bool `json:"allowEncodedNullCharacter,omitempty" toml:"allowEncodedNullCharacter,omitempty" yaml:"allowEncodedNullCharacter,omitempty" export:"true"`
// AllowEncodedSemicolon defines whether requests with encoded semicolon characters in the path are allowed.
AllowEncodedSemicolon bool `json:"allowEncodedSemicolon,omitempty" toml:"allowEncodedSemicolon,omitempty" yaml:"allowEncodedSemicolon,omitempty" export:"true"`
// AllowEncodedPercent defines whether requests with encoded percent characters in the path are allowed.
AllowEncodedPercent bool `json:"allowEncodedPercent,omitempty" toml:"allowEncodedPercent,omitempty" yaml:"allowEncodedPercent,omitempty" export:"true"`
// AllowEncodedQuestionMark defines whether requests with encoded question mark characters in the path are allowed.
AllowEncodedQuestionMark bool `json:"allowEncodedQuestionMark,omitempty" toml:"allowEncodedQuestionMark,omitempty" yaml:"allowEncodedQuestionMark,omitempty" export:"true"`
// AllowEncodedHash defines whether requests with encoded hash characters in the path are allowed.
AllowEncodedHash bool `json:"allowEncodedHash,omitempty" toml:"allowEncodedHash,omitempty" yaml:"allowEncodedHash,omitempty" export:"true"`
}
// +k8s:deepcopy-gen=true
// ErrorPage holds the custom error middleware configuration.
// This middleware returns a custom page in lieu of the default, according to configured ranges of HTTP Status codes.
type ErrorPage struct {
@@ -306,6 +306,22 @@ func (in *DigestAuth) DeepCopy() *DigestAuth {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EncodedCharacters) DeepCopyInto(out *EncodedCharacters) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EncodedCharacters.
func (in *EncodedCharacters) DeepCopy() *EncodedCharacters {
if in == nil {
return nil
}
out := new(EncodedCharacters)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ErrorPage) DeepCopyInto(out *ErrorPage) {
*out = *in
@@ -905,6 +921,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
*out = new(Headers)
(*in).DeepCopyInto(*out)
}
if in.EncodedCharacters != nil {
in, out := &in.EncodedCharacters, &out.EncodedCharacters
*out = new(EncodedCharacters)
**out = **in
}
if in.Errors != nil {
in, out := &in.Errors, &out.Errors
*out = new(ErrorPage)
@@ -0,0 +1,100 @@
package encodedcharacters
import (
"context"
"net/http"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
)
const typeName = "EncodedCharacters"
type encodedCharacters struct {
next http.Handler
deniedCharacters map[string]struct{}
name string
}
// NewEncodedCharacters creates an Encoded Characters middleware.
func NewEncodedCharacters(ctx context.Context, next http.Handler, config dynamic.EncodedCharacters, name string) http.Handler {
middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware")
return &encodedCharacters{
next: next,
deniedCharacters: mapDeniedCharacters(config),
name: name,
}
}
func (ec *encodedCharacters) GetTracingInformation() (string, string) {
return ec.name, typeName
}
func (ec *encodedCharacters) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := middlewares.GetLogger(req.Context(), ec.name, typeName)
if len(ec.deniedCharacters) == 0 {
ec.next.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 := ec.deniedCharacters[escapedPath[i:i+3]]; exists {
logger.Debug().Msgf("Rejecting request because it contains encoded character %s in the URL path: %s", escapedPath[i:i+3], escapedPath)
rw.WriteHeader(http.StatusBadRequest)
return
}
i += 2
}
ec.next.ServeHTTP(rw, req)
}
// mapDeniedCharacters returns a map of unallowed encoded characters.
func mapDeniedCharacters(config dynamic.EncodedCharacters) map[string]struct{} {
characters := make(map[string]struct{})
if !config.AllowEncodedSlash {
characters["%2F"] = struct{}{}
characters["%2f"] = struct{}{}
}
if !config.AllowEncodedBackSlash {
characters["%5C"] = struct{}{}
characters["%5c"] = struct{}{}
}
if !config.AllowEncodedNullCharacter {
characters["%00"] = struct{}{}
}
if !config.AllowEncodedSemicolon {
characters["%3B"] = struct{}{}
characters["%3b"] = struct{}{}
}
if !config.AllowEncodedPercent {
characters["%25"] = struct{}{}
}
if !config.AllowEncodedQuestionMark {
characters["%3F"] = struct{}{}
characters["%3f"] = struct{}{}
}
if !config.AllowEncodedHash {
characters["%23"] = struct{}{}
}
return characters
}
@@ -0,0 +1,191 @@
package encodedcharacters
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
)
func TestEncodedCharacters(t *testing.T) {
testCases := []struct {
desc string
config dynamic.EncodedCharacters
path string
expectedStatusCode int
}{
{
desc: "deny encoded slash",
config: dynamic.EncodedCharacters{},
path: "/foo%2fbar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded slash",
config: dynamic.EncodedCharacters{
AllowEncodedSlash: true,
},
path: "/foo%2fbar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded backslash",
config: dynamic.EncodedCharacters{},
path: "/foo%5cbar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded backslash",
config: dynamic.EncodedCharacters{
AllowEncodedBackSlash: true,
},
path: "/foo%5cbar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded null character",
config: dynamic.EncodedCharacters{},
path: "/foo%00bar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded null character",
config: dynamic.EncodedCharacters{
AllowEncodedNullCharacter: true,
},
path: "/foo%00bar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded semi colon",
config: dynamic.EncodedCharacters{},
path: "/foo%3bbar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded semi colon",
config: dynamic.EncodedCharacters{
AllowEncodedSemicolon: true,
},
path: "/foo%3bbar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded percent",
config: dynamic.EncodedCharacters{},
path: "/foo%25bar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded percent",
config: dynamic.EncodedCharacters{
AllowEncodedPercent: true,
},
path: "/foo%25bar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded question mark",
config: dynamic.EncodedCharacters{},
path: "/foo%3fbar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded question mark",
config: dynamic.EncodedCharacters{
AllowEncodedQuestionMark: true,
},
path: "/foo%3fbar",
expectedStatusCode: http.StatusOK,
},
{
desc: "deny encoded hash",
config: dynamic.EncodedCharacters{},
path: "/foo%23bar",
expectedStatusCode: http.StatusBadRequest,
},
{
desc: "allow encoded hash",
config: dynamic.EncodedCharacters{
AllowEncodedHash: true,
},
path: "/foo%23bar",
expectedStatusCode: http.StatusOK,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
handler := NewEncodedCharacters(t.Context(), next, test.config, "test-encoded-characters")
req := httptest.NewRequest(http.MethodGet, test.path, nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
require.Equal(t, test.expectedStatusCode, recorder.Code)
})
}
}
func TestMapDeniedCharacters(t *testing.T) {
testCases := []struct {
desc string
config dynamic.EncodedCharacters
expectedDeniedChar map[string]struct{}
}{
{
desc: "deny all characters",
config: dynamic.EncodedCharacters{},
expectedDeniedChar: map[string]struct{}{
"%2F": {}, "%2f": {}, // slash
"%5C": {}, "%5c": {}, // backslash
"%00": {}, // null
"%3B": {}, "%3b": {}, // semicolon
"%25": {}, // percent
"%3F": {}, "%3f": {}, // question mark
"%23": {}, // hash
},
},
{
desc: "allow only encoded slash",
config: dynamic.EncodedCharacters{
AllowEncodedSlash: true,
},
expectedDeniedChar: map[string]struct{}{
"%5C": {}, "%5c": {}, // backslash
"%00": {}, // null
"%3B": {}, "%3b": {}, // semicolon
"%25": {}, // percent
"%3F": {}, "%3f": {}, // question mark
"%23": {}, // hash
},
},
{
desc: "allow all characters",
config: dynamic.EncodedCharacters{
AllowEncodedSlash: true,
AllowEncodedBackSlash: true,
AllowEncodedNullCharacter: true,
AllowEncodedSemicolon: true,
AllowEncodedPercent: true,
AllowEncodedQuestionMark: true,
AllowEncodedHash: true,
},
expectedDeniedChar: map[string]struct{}{},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
deniedMap := mapDeniedCharacters(test.config)
require.Equal(t, test.expectedDeniedChar, deniedMap)
require.Len(t, deniedMap, len(test.expectedDeniedChar))
})
}
}
@@ -43,6 +43,7 @@ type MiddlewareSpecApplyConfiguration struct {
IPWhiteList *dynamic.IPWhiteList `json:"ipWhiteList,omitempty"`
IPAllowList *dynamic.IPAllowList `json:"ipAllowList,omitempty"`
Headers *dynamic.Headers `json:"headers,omitempty"`
EncodedCharacters *dynamic.EncodedCharacters `json:"encodedCharacters,omitempty"`
Errors *ErrorPageApplyConfiguration `json:"errors,omitempty"`
RateLimit *RateLimitApplyConfiguration `json:"rateLimit,omitempty"`
RedirectRegex *dynamic.RedirectRegex `json:"redirectRegex,omitempty"`
@@ -139,6 +140,14 @@ func (b *MiddlewareSpecApplyConfiguration) WithHeaders(value dynamic.Headers) *M
return b
}
// WithEncodedCharacters sets the EncodedCharacters field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the EncodedCharacters field is set to the value of the last call.
func (b *MiddlewareSpecApplyConfiguration) WithEncodedCharacters(value dynamic.EncodedCharacters) *MiddlewareSpecApplyConfiguration {
b.EncodedCharacters = &value
return b
}
// WithErrors sets the Errors field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Errors field is set to the value of the last call.
@@ -312,6 +312,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
IPWhiteList: middleware.Spec.IPWhiteList,
IPAllowList: middleware.Spec.IPAllowList,
Headers: middleware.Spec.Headers,
EncodedCharacters: middleware.Spec.EncodedCharacters,
Errors: errorPage,
RateLimit: rateLimit,
RedirectRegex: middleware.Spec.RedirectRegex,
@@ -36,6 +36,7 @@ type MiddlewareSpec struct {
IPWhiteList *dynamic.IPWhiteList `json:"ipWhiteList,omitempty"`
IPAllowList *dynamic.IPAllowList `json:"ipAllowList,omitempty"`
Headers *dynamic.Headers `json:"headers,omitempty"`
EncodedCharacters *dynamic.EncodedCharacters `json:"encodedCharacters,omitempty"`
Errors *ErrorPage `json:"errors,omitempty"`
RateLimit *RateLimit `json:"rateLimit,omitempty"`
RedirectRegex *dynamic.RedirectRegex `json:"redirectRegex,omitempty"`
@@ -858,6 +858,11 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) {
*out = new(dynamic.Headers)
(*in).DeepCopyInto(*out)
}
if in.EncodedCharacters != nil {
in, out := &in.EncodedCharacters, &out.EncodedCharacters
*out = new(dynamic.EncodedCharacters)
**out = **in
}
if in.Errors != nil {
in, out := &in.Errors, &out.Errors
*out = new(ErrorPage)
+11
View File
@@ -20,6 +20,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/compress"
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
"github.com/traefik/traefik/v3/pkg/middlewares/encodedcharacters"
"github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/headermodifier"
gapiredirect "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/redirect"
"github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/urlrewrite"
@@ -192,6 +193,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
}
}
// EncodedCharacters
if config.EncodedCharacters != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return encodedcharacters.NewEncodedCharacters(ctx, next, *config.EncodedCharacters, middlewareName), nil
}
}
// CustomErrors
if config.Errors != nil {
if middleware != nil {