diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 45843a5fa7..20dd433b7a 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -147,6 +147,9 @@ Defines whether requests with encoded slash characters in the path are allowed. `--entrypoints..http.encodequerysemicolons`: Defines whether request query semicolons should be URLEncoded. (Default: ```false```) +`--entrypoints..http.maxheaderbytes`: +Maximum size of request headers in bytes. (Default: ```1048576```) + `--entrypoints..http.middlewares`: Default middlewares for the routers linked to the entry point. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 52b31654dc..ea50de0a2f 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -156,6 +156,9 @@ Defines whether requests with encoded slash characters in the path are allowed. `TRAEFIK_ENTRYPOINTS__HTTP_ENCODEQUERYSEMICOLONS`: Defines whether request query semicolons should be URLEncoded. (Default: ```false```) +`TRAEFIK_ENTRYPOINTS__HTTP_MAXHEADERBYTES`: +Maximum size of request headers in bytes. (Default: ```1048576```) + `TRAEFIK_ENTRYPOINTS__HTTP_MIDDLEWARES`: Default middlewares for the routers linked to the entry point. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index efac83ab87..1c168d0f35 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -38,6 +38,7 @@ middlewares = ["foobar", "foobar"] encodeQuerySemicolons = true sanitizePath = true + maxHeaderBytes = 42 [entryPoints.EntryPoint0.http.redirections] [entryPoints.EntryPoint0.http.redirections.entryPoint] to = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 3d49c68dba..56352910ad 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -72,6 +72,7 @@ entryPoints: allowEncodedHash: true encodeQuerySemicolons: true sanitizePath: true + maxHeaderBytes: 42 http2: maxConcurrentStreams: 42 http3: diff --git a/integration/fixtures/simple_max_header_size.toml b/integration/fixtures/simple_max_header_size.toml new file mode 100644 index 0000000000..afd1dd7e27 --- /dev/null +++ b/integration/fixtures/simple_max_header_size.toml @@ -0,0 +1,25 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[entryPoints] + [entryPoints.web] + address = ":8000" + [entryPoints.web.http] + maxHeaderBytes = 1310720 + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.test-router] + entryPoints = ["web"] + service = "test-service" + rule = "Host(`127.0.0.1`)" + +[http.services] + [http.services.test-service] + [[http.services.test-service.loadBalancer.servers]] + url = "{{ .TestServer }}" diff --git a/integration/simple_test.go b/integration/simple_test.go index 10cdbeaaf8..b756825213 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1616,3 +1616,63 @@ func waitForWritePartial(t *testing.T, conn net.Conn) { t.Fatalf("timeout waiting for connection timeout") } } + +func (s *SimpleSuite) TestMaxHeaderBytes() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + listener, err := net.Listen("tcp", "127.0.0.1:9000") + require.NoError(s.T(), err) + + ts := &httptest.Server{ + Listener: listener, + Config: &http.Server{ + Handler: handler, + MaxHeaderBytes: 1.25 * 1024 * 1024, // 1.25 MB + }, + } + ts.Start() + defer ts.Close() + + // The test server and traefik config file both specify a max request header size of 1.25 MB. + file := s.adaptFile("fixtures/simple_max_header_size.toml", struct { + TestServer string + }{ts.URL}) + + s.traefikCmd(withConfigFile(file)) + + testCases := []struct { + name string + headerSize int + expectedStatus int + }{ + { + name: "1.25MB header", + headerSize: int(1.25 * 1024 * 1024), + expectedStatus: http.StatusOK, + }, + { + name: "1.5MB header", + headerSize: int(1.5 * 1024 * 1024), + expectedStatus: http.StatusRequestHeaderFieldsTooLarge, + }, + { + name: "500KB header", + headerSize: int(500 * 1024), + expectedStatus: http.StatusOK, + }, + } + + for _, test := range testCases { + s.Run(test.name, func() { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil) + require.NoError(s.T(), err) + + req.Header.Set("X-Large-Header", strings.Repeat("A", test.headerSize)) + + err = try.Request(req, 2*time.Second, try.StatusCodeIs(test.expectedStatus)) + require.NoError(s.T(), err) + }) + } +} diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index ad98636430..8ed266d2ac 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -3,6 +3,7 @@ package static import ( "fmt" "math" + "net/http" "strings" ptypes "github.com/traefik/paerser/types" @@ -52,6 +53,7 @@ func (ep *EntryPoint) SetDefaults() { ep.ForwardedHeaders = &ForwardedHeaders{} ep.UDP = &UDPConfig{} ep.UDP.SetDefaults() + ep.HTTP = HTTPConfig{} ep.HTTP.SetDefaults() ep.HTTP2 = &HTTP2Config{} ep.HTTP2.SetDefaults() @@ -65,12 +67,14 @@ type HTTPConfig struct { 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"` + MaxHeaderBytes int `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"` } // SetDefaults sets the default values. func (h *HTTPConfig) SetDefaults() { sanitizePath := true h.SanitizePath = &sanitizePath + h.MaxHeaderBytes = http.DefaultMaxHeaderBytes } // EncodedCharacters configures which encoded characters are allowed in the request path. diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 252ec0514a..564ee0f750 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -640,12 +640,13 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati } serverHTTP := &http.Server{ - Protocols: &protocols, - Handler: handler, - ErrorLog: httpServerLogger, - ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout), - WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout), - IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout), + Protocols: &protocols, + Handler: handler, + ErrorLog: httpServerLogger, + ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout), + WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout), + IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout), + MaxHeaderBytes: configuration.HTTP.MaxHeaderBytes, HTTP2: &http.HTTP2Config{ MaxConcurrentStreams: int(configuration.HTTP2.MaxConcurrentStreams), },