mirror of
https://github.com/traefik/traefik
synced 2026-02-03 08:50:32 +00:00
Add encodedCharacters middleware
This commit is contained in:
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
+9
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user