diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 8321f1d04c..6dd45615c4 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -775,3 +775,13 @@ However, it is strongly recommended to set this option to a suitable value to av such as DoS attacks and memory exhaustion. Please check out the [ForwardAuth](../middlewares/http/forwardauth.md#maxresponsebodysize) middleware documentation for more details. + +## v2.11.41 + +### `maxResponseBodySize` configuration on HTTP provider + +In `v2.11.41`, a new `maxResponseBodySize` option has been added to the HTTP provider configuration. +The default value for this option is -1, which means there is no limit to the response body size. +However, it is strongly recommended to set this option to a suitable value to avoid performance issues such as memory exhaustion. + +Please check out the [HTTP](../providers/http.md#maxresponsebodysize) provider documentation for more details. diff --git a/docs/content/providers/http.md b/docs/content/providers/http.md index f99e90a7cd..a62b73d6fa 100644 --- a/docs/content/providers/http.md +++ b/docs/content/providers/http.md @@ -178,3 +178,25 @@ providers: ```bash tab="CLI" --providers.http.tls.insecureSkipVerify=true ``` + +### `maxResponseBodySize` + +_Optional, Default=-1_ + +Defines the maximum size of the response body in bytes. +If left unset (or set to -1), the response body size is unrestricted which can have performance implications. + +```yaml tab="File (YAML)" +providers: + http: + maxResponseBodySize: -1 +``` + +```toml tab="File (TOML)" +[providers.http] + maxResponseBodySize = -1 +``` + +```bash tab="CLI" +--providers.http.maxResponseBodySize=-1 +``` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index cec0dafd3d..fee7886054 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -663,6 +663,9 @@ Enable HTTP backend with default settings. (Default: ```false```) `--providers.http.endpoint`: Load configuration from this endpoint. +`--providers.http.maxresponsebodysize`: +Defines the maximum size of the response body in bytes. (Default: ```-1```) + `--providers.http.pollinterval`: Polling interval for endpoint. (Default: ```5```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 6947150faf..e6fef29eac 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -663,6 +663,9 @@ Enable HTTP backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_HTTP_ENDPOINT`: Load configuration from this endpoint. +`TRAEFIK_PROVIDERS_HTTP_MAXRESPONSEBODYSIZE`: +Defines the maximum size of the response body in bytes. (Default: ```-1```) + `TRAEFIK_PROVIDERS_HTTP_POLLINTERVAL`: Polling interval for endpoint. (Default: ```5```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index a69e2406eb..94758e5d61 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -274,6 +274,7 @@ endpoint = "foobar" pollInterval = "42s" pollTimeout = "42s" + maxResponseBodySize = 42 [providers.http.tls] ca = "foobar" caOptional = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 99a08fccb0..873595b6b9 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -310,6 +310,7 @@ providers: cert: foobar key: foobar insecureSkipVerify: true + maxResponseBodySize: 42 plugin: PluginConf0: name0: foobar diff --git a/pkg/provider/http/http.go b/pkg/provider/http/http.go index 15c25589e8..1a61d3cb91 100644 --- a/pkg/provider/http/http.go +++ b/pkg/provider/http/http.go @@ -23,6 +23,8 @@ import ( var _ provider.Provider = (*Provider)(nil) +const defaultMaxResponseBodySize = -1 + // Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration. type Provider struct { Endpoint string `description:"Load configuration from this endpoint." json:"endpoint" toml:"endpoint" yaml:"endpoint"` @@ -31,12 +33,14 @@ type Provider struct { TLS *types.ClientTLS `description:"Enable TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` httpClient *http.Client lastConfigurationHash uint64 + MaxResponseBodySize int64 `description:"Defines the maximum size of the response body in bytes." json:"maxResponseBodySize,omitempty" toml:"maxResponseBodySize,omitempty" yaml:"maxResponseBodySize,omitempty" export:"true"` } // SetDefaults sets the default values. func (p *Provider) SetDefaults() { p.PollInterval = ptypes.Duration(5 * time.Second) p.PollTimeout = ptypes.Duration(5 * time.Second) + p.MaxResponseBodySize = defaultMaxResponseBodySize } // Init the provider. @@ -151,7 +155,19 @@ func (p *Provider) fetchConfigurationData() ([]byte, error) { return nil, fmt.Errorf("received non-ok response code: %d", res.StatusCode) } - return io.ReadAll(res.Body) + if p.MaxResponseBodySize < 0 { + return io.ReadAll(res.Body) + } + + data, err := io.ReadAll(io.LimitReader(res.Body, p.MaxResponseBodySize+1)) + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) + } + if int64(len(data)) > p.MaxResponseBodySize { + return nil, errors.New("response body too large") + } + + return data, nil } // decodeConfiguration decodes and returns the dynamic configuration from the given data. diff --git a/pkg/provider/http/http_test.go b/pkg/provider/http/http_test.go index cb7d8cdb59..11e050f3cc 100644 --- a/pkg/provider/http/http_test.go +++ b/pkg/provider/http/http_test.go @@ -13,6 +13,7 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/tls" + "k8s.io/utils/ptr" ) func TestProvider_Init(t *testing.T) { @@ -64,14 +65,16 @@ func TestProvider_SetDefaults(t *testing.T) { assert.Equal(t, provider.PollInterval, ptypes.Duration(5*time.Second)) assert.Equal(t, provider.PollTimeout, ptypes.Duration(5*time.Second)) + assert.Equal(t, int64(-1), provider.MaxResponseBodySize) } func TestProvider_fetchConfigurationData(t *testing.T) { tests := []struct { - desc string - handler func(rw http.ResponseWriter, req *http.Request) - expData []byte - expErr bool + desc string + handler func(rw http.ResponseWriter, req *http.Request) + expData []byte + expErr bool + maxResponseBodySize *int64 }{ { desc: "should return the fetched configuration data", @@ -88,6 +91,34 @@ func TestProvider_fetchConfigurationData(t *testing.T) { rw.WriteHeader(http.StatusNoContent) }, }, + { + desc: "should return an error response body is too long when maxResponseBodySize is 0", + maxResponseBodySize: ptr.To(int64(0)), + expErr: true, + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(rw, "{}") + }, + }, + { + desc: "should return an error response body is too long when response is longer than maxResponseBodySize", + maxResponseBodySize: ptr.To(int64(1)), + expErr: true, + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(rw, "{}") + }, + }, + { + desc: "should return the fetched configuration data when response is the same length with maxResponseBodySize", + maxResponseBodySize: ptr.To(int64(2)), + expData: []byte("{}"), + expErr: false, + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(rw, "{}") + }, + }, } for _, test := range tests { @@ -95,10 +126,14 @@ func TestProvider_fetchConfigurationData(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(test.handler)) defer server.Close() - provider := Provider{ - Endpoint: server.URL, - PollInterval: ptypes.Duration(1 * time.Second), - PollTimeout: ptypes.Duration(1 * time.Second), + var provider Provider + provider.SetDefaults() + + provider.Endpoint = server.URL + provider.PollTimeout = ptypes.Duration(1 * time.Second) + provider.PollInterval = ptypes.Duration(100 * time.Millisecond) + if test.maxResponseBodySize != nil { + provider.MaxResponseBodySize = *test.maxResponseBodySize } err := provider.Init() @@ -179,11 +214,12 @@ func TestProvider_Provide(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() - provider := Provider{ - Endpoint: server.URL, - PollTimeout: ptypes.Duration(1 * time.Second), - PollInterval: ptypes.Duration(100 * time.Millisecond), - } + var provider Provider + provider.SetDefaults() + + provider.Endpoint = server.URL + provider.PollTimeout = ptypes.Duration(1 * time.Second) + provider.PollInterval = ptypes.Duration(100 * time.Millisecond) err := provider.Init() require.NoError(t, err) @@ -234,11 +270,12 @@ func TestProvider_ProvideConfigurationOnlyOnceIfUnchanged(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() - provider := Provider{ - Endpoint: server.URL + "/endpoint", - PollTimeout: ptypes.Duration(1 * time.Second), - PollInterval: ptypes.Duration(100 * time.Millisecond), - } + var provider Provider + provider.SetDefaults() + + provider.Endpoint = server.URL + "/endpoint" + provider.PollTimeout = ptypes.Duration(1 * time.Second) + provider.PollInterval = ptypes.Duration(100 * time.Millisecond) err := provider.Init() require.NoError(t, err)