diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index b0c02c565..617359216 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -141,6 +141,9 @@ HTTP configuration. `--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 2f4c19971..045da7830 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -150,6 +150,9 @@ UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) `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 f92cd3763..8f899b211 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -51,6 +51,7 @@ [entryPoints.EntryPoint0.http] middlewares = ["foobar", "foobar"] encodeQuerySemicolons = 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 12b5efccc..31b8e485f 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -80,6 +80,7 @@ entryPoints: - foobar - foobar encodeQuerySemicolons: 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 000000000..afd1dd7e2 --- /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 9aae5c1b2..e57fb2c9a 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1511,3 +1511,63 @@ func (s *SimpleSuite) TestDenyFragment() { require.NoError(s.T(), err) assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) } + +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 9b48ddce4..bc3f3c30c 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" @@ -53,6 +54,8 @@ 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() } @@ -63,6 +66,12 @@ type HTTPConfig struct { 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"` EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"` + 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 (c *HTTPConfig) SetDefaults() { + c.MaxHeaderBytes = http.DefaultMaxHeaderBytes } // HTTP2Config is the HTTP2 configuration of an entry point. diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index ca901c90f..e40244219 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -633,11 +633,12 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati } serverHTTP := &http.Server{ - Handler: handler, - ErrorLog: stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0), - ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout), - WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout), - IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout), + Handler: handler, + ErrorLog: stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0), + 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, } if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {