From b460351f7e7cc3c560bf87e57a2d3996319ca5e1 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:24:05 +0100 Subject: [PATCH 1/5] Add maxResponseBodySize configuration on HTTP provider --- docs/content/migration/v2.md | 10 +++ docs/content/providers/http.md | 22 ++++++ .../reference/static-configuration/cli-ref.md | 3 + .../reference/static-configuration/env-ref.md | 3 + .../reference/static-configuration/file.toml | 1 + .../reference/static-configuration/file.yaml | 1 + pkg/provider/http/http.go | 18 ++++- pkg/provider/http/http_test.go | 73 ++++++++++++++----- 8 files changed, 112 insertions(+), 19 deletions(-) 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) From 832f48d9bf235787569535887337bb150a8dde13 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 11 Mar 2026 17:56:06 +0100 Subject: [PATCH 2/5] Support fragmented TLS client hello Co-authored-by: Kevin Pollet --- pkg/server/router/tcp/router.go | 79 ++++++++----------------- pkg/server/router/tcp/router_test.go | 86 ++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 77 deletions(-) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 8b98c853d1..23f624c0c1 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -19,15 +19,8 @@ import ( "github.com/traefik/traefik/v2/pkg/tcp" ) -const ( - defaultBufSize = 4096 - // Per RFC 8446 Section 5.1, the maximum TLS record payload length is 2^14 (16384) bytes. - // A ClientHello is always a plaintext record, so any value exceeding this limit is invalid - // and likely indicates an attack attempting to force oversized per-connection buffer allocations. - // However, in practice the go server handshake can read up to 16384 + 2048 bytes, - // so we need to allow for some extra bytes to avoid rejecting valid handshakes. - maxTLSRecordLen = 16384 + 2048 -) +// errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello. +var errClientHelloRead = errors.New("client hello successfully read") // Router is a TCP router. type Router struct { @@ -127,8 +120,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } // TODO -- Check if ProxyProtocol changes the first bytes of the request - br := bufio.NewReader(conn) - hello, err := clientHelloInfo(br) + hello, err := clientHelloInfo(conn) if err != nil { var opErr *net.OpError if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || !opErr.Timeout()) { @@ -370,7 +362,10 @@ type clientHello struct { // clientHelloInfo returns various data from the clientHello handshake, // without consuming any bytes from br. // It returns an error if it can't peek the first byte from the connection. -func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { +func clientHelloInfo(conn net.Conn) (*clientHello, error) { + var peeked bytes.Buffer + br := bufio.NewReader(io.TeeReader(conn, &peeked)) + hdr, err := br.Peek(1) if err != nil { return nil, fmt.Errorf("peeking first byte: %w", err) @@ -386,73 +381,49 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { // we consider SSLv2 as TLS, and it will be refused by real TLS handshake. return &clientHello{ isTLS: true, - peeked: getPeeked(br), + peeked: peeked.String(), }, nil } return &clientHello{ - peeked: getPeeked(br), + peeked: peeked.String(), }, nil // Not TLS. } - const recordHeaderLen = 5 - hdr, err = br.Peek(recordHeaderLen) - if err != nil { - return nil, fmt.Errorf("peeking client hello headers: %w", err) - } - - recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3] - - if recLen > maxTLSRecordLen { - return nil, fmt.Errorf("peeking client hello bytes, oversized record: %d", recLen) - } - - if recordHeaderLen+recLen > defaultBufSize { - br = bufio.NewReaderSize(br, recordHeaderLen+recLen) - } - - helloBytes, err := br.Peek(recordHeaderLen + recLen) - if err != nil { - return nil, fmt.Errorf("peeking client hello bytes: %w", err) - } - - sni := "" - var protos []string - server := tls.Server(helloSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{ + var ( + sni string + protos []string + ) + server := tls.Server(readOnlyConn{r: br}, &tls.Config{ GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { sni = hello.ServerName protos = hello.SupportedProtos - return nil, nil + // This error prevents unnecessary additional steps in the TLS ClientHello message processing. + return nil, errClientHelloRead }, }) - _ = server.Handshake() + + if handshakeErr := server.Handshake(); !errors.Is(handshakeErr, errClientHelloRead) { + return nil, fmt.Errorf("reading client hello: %w", handshakeErr) + } return &clientHello{ serverName: sni, isTLS: true, - peeked: getPeeked(br), + peeked: peeked.String(), protos: protos, }, nil } -func getPeeked(br *bufio.Reader) string { - peeked, err := br.Peek(br.Buffered()) - if err != nil { - log.WithoutContext().Errorf("Error while peeking bytes: %s", err) - return "" - } - return string(peeked) -} - -// helloSniffConn is a net.Conn that reads from r, fails on Writes, +// readOnlyConn is a net.Conn that reads from r, fails on Writes, // and crashes otherwise. -type helloSniffConn struct { +type readOnlyConn struct { net.Conn // nil; crash on any unexpected use r io.Reader } // Read reads from the underlying reader. -func (c helloSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) } +func (c readOnlyConn) Read(p []byte) (int, error) { return c.r.Read(p) } // Write crashes all the time. -func (helloSniffConn) Write(p []byte) (int, error) { return 0, io.EOF } +func (readOnlyConn) Write(_ []byte) (int, error) { return 0, io.EOF } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 837923e57c..e12a564610 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -1,7 +1,6 @@ package tcp import ( - "bufio" "bytes" "crypto/tls" "errors" @@ -1104,8 +1103,7 @@ func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) { resultCh := make(chan result, 1) go func() { - br := bufio.NewReader(serverConn) - hello, err := clientHelloInfo(br) + hello, err := clientHelloInfo(serverConn) resultCh <- result{hello, err} }() @@ -1133,12 +1131,34 @@ func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) { } } -// Test_clientHelloInfo_validRecordLength verifies that clientHelloInfo -// still works correctly with legitimate TLS record sizes. -func Test_clientHelloInfo_validRecordLength(t *testing.T) { +// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation: +// clientHelloInfo only reads a single TLS record. When a ClientHello handshake +// message is split across multiple TLS records (RFC 5246 ยง6.2.1), the SNI cannot +// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed. +func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) { + serverName := "foo.example.com" + record := buildClientHelloRecord(t, serverName) + + const hdrLen = 5 + payload := record[hdrLen:] + + ver1, ver2 := record[1], record[2] + + var recordsData bytes.Buffer + for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} { + recordsData.WriteByte(0x16) + recordsData.WriteByte(ver1) + recordsData.WriteByte(ver2) + recordsData.WriteByte(byte(len(part) >> 8)) + recordsData.WriteByte(byte(len(part))) + recordsData.Write(part) + } + serverConn, clientConn := net.Pipe() - defer serverConn.Close() - defer clientConn.Close() + t.Cleanup(func() { + _ = serverConn.Close() + _ = clientConn.Close() + }) type result struct { hello *clientHello @@ -1147,31 +1167,51 @@ func Test_clientHelloInfo_validRecordLength(t *testing.T) { resultCh := make(chan result, 1) go func() { - br := bufio.NewReader(serverConn) - hello, err := clientHelloInfo(br) + hello, err := clientHelloInfo(serverConn) resultCh <- result{hello, err} }() - // Build a TLS record header with a small (valid) record length. - recLen := 100 - hdr := []byte{ - 0x16, // Content Type: Handshake - 0x03, 0x03, // Version: TLS 1.2 - byte(recLen >> 8), // Length high byte - byte(recLen & 0xFF), // Length low byte - } - payload := make([]byte, recLen) - - _, err := clientConn.Write(append(hdr, payload...)) + _, err := clientConn.Write(recordsData.Bytes()) require.NoError(t, err) - clientConn.Close() + _ = clientConn.Close() select { case r := <-resultCh: require.NoError(t, r.err) require.NotNil(t, r.hello) assert.True(t, r.hello.isTLS) + assert.Equal(t, serverName, r.hello.serverName) case <-time.After(5 * time.Second): - t.Fatal("clientHelloInfo blocked on valid TLS record") + t.Fatal("clientHelloInfo blocked") } } + +// buildClientHelloRecord captures a real TLS ClientHello record from Go's TLS stack +// for the given serverName. +// It returns the raw record bytes and the byte offset of the SNI value within those bytes. +func buildClientHelloRecord(t *testing.T, serverName string) []byte { + t.Helper() + + serverConn, clientConn := net.Pipe() + + recordCh := make(chan []byte, 1) + go func() { + buf := make([]byte, 65536) + n, _ := serverConn.Read(buf) + _ = serverConn.Close() + recordCh <- buf[:n] + }() + + go func() { + tlsConn := tls.Client(clientConn, &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: true, //nolint:gosec + }) + _ = tlsConn.Handshake() + _ = clientConn.Close() + }() + + record := <-recordCh + + return record +} From a377b3aba1adb059dc6b5c2a4d0ce7510ed8397e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20BUISSON?= Date: Fri, 13 Mar 2026 16:58:04 +0100 Subject: [PATCH 3/5] Bump mkdocs-traefiklabs to use consent mode --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ba7f3a058d..95e073aaf7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ mkdocs==1.4.3 mkdocs-include-markdown-plugin==7.2.0 mkdocs-exclude==1.0.2 -mkdocs-traefiklabs>=100.0.7 +mkdocs-traefiklabs>=100.1.0 click==8.1.7 colorama==0.4.6 From 122175ac2f728aebabfd749cf328962e4c2d4944 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 17 Mar 2026 15:36:05 +0100 Subject: [PATCH 4/5] Make basic auth check timing constant Co-authored-by: Kevin Pollet --- docs/content/middlewares/http/basicauth.md | 6 +++++ pkg/middlewares/auth/basic_auth.go | 31 +++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/content/middlewares/http/basicauth.md b/docs/content/middlewares/http/basicauth.md index 08ab3dc42e..ffc6842264 100644 --- a/docs/content/middlewares/http/basicauth.md +++ b/docs/content/middlewares/http/basicauth.md @@ -12,6 +12,12 @@ Adding Basic Authentication The BasicAuth middleware grants access to services to authorized users only. +!!! warning "Timing attacks" + + The BasicAuth middleware is vulnerable to timing attacks when the configured users' password hashes do not use the same algorithm and cost. + However, when the configured user's password hashes are of the same algorithm and cost, the middleware guarantees the same comparison time between existing and non-existing users. + This prevents an attacker from leveraging the time difference to determine whether a user exists. + ## Configuration Examples ```yaml tab="Docker" diff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go index b14a080614..944900ef1f 100644 --- a/pkg/middlewares/auth/basic_auth.go +++ b/pkg/middlewares/auth/basic_auth.go @@ -3,8 +3,10 @@ package auth import ( "context" "fmt" + "maps" "net/http" "net/url" + "slices" "strings" goauth "github.com/abbot/go-http-auth" @@ -27,6 +29,8 @@ type basicAuth struct { headerField string removeHeader bool name string + + notFoundSecret string } // NewBasic creates a basicAuth middleware. @@ -37,12 +41,18 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu return nil, err } + // To prevent timing attacks, we need to compute a hash even if the user is not found. + // We assume it to be safe only when the users hashes are all from the same algorithm, + // so we can pick the first one as a random hash to compute. + notFoundSecret := users[slices.Collect(maps.Values(users))[0]] + ba := &basicAuth{ - next: next, - users: users, - headerField: authConfig.HeaderField, - removeHeader: authConfig.RemoveHeader, - name: name, + next: next, + users: users, + headerField: authConfig.HeaderField, + removeHeader: authConfig.RemoveHeader, + name: name, + notFoundSecret: notFoundSecret, } realm := defaultRealm @@ -63,10 +73,13 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), b.name, basicTypeName)) user, password, ok := req.BasicAuth() + var authenticated bool if ok { secret := b.auth.Secrets(user, b.auth.Realm) - if secret == "" || !goauth.CheckSecret(password, secret) { - ok = false + if secret != "" { + authenticated = goauth.CheckSecret(password, secret) + } else { + _ = goauth.CheckSecret(password, b.notFoundSecret) } } @@ -75,7 +88,7 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { logData.Core[accesslog.ClientUsername] = user } - if !ok { + if !authenticated { logger.Debug("Authentication failed") tracing.SetErrorWithEvent(req, "Authentication failed") @@ -97,7 +110,7 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { b.next.ServeHTTP(rw, req) } -func (b *basicAuth) secretBasic(user, realm string) string { +func (b *basicAuth) secretBasic(user, _ string) string { if secret, ok := b.users[user]; ok { return secret } From f2c198c9f36e97895f045e83ad2050ad21e52f86 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 18 Mar 2026 10:40:06 +0100 Subject: [PATCH 5/5] Prepare release v2.11.41 --- CHANGELOG.md | 11 +++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff06e55992..2468c476b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [v2.11.41](https://github.com/traefik/traefik/tree/v2.11.41) (2026-03-18) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.40...v2.11.41) + +**Bug fixes:** +- **[http]** Add maxResponseBodySize configuration on HTTP provider ([#12788](https://github.com/traefik/traefik/pull/12788) @gndz07) +- **[tls]** Support fragmented TLS client hello ([#12787](https://github.com/traefik/traefik/pull/12787) @rtribotte) +- **[middleware, authentication]** Make basic auth check timing constant ([#12803](https://github.com/traefik/traefik/pull/12803) @rtribotte) + +**Documentation:** +- Bump mkdocs-traefiklabs to use consent mode ([#12804](https://github.com/traefik/traefik/pull/12804) @darkweaver87) + ## [v2.11.40](https://github.com/traefik/traefik/tree/v2.11.40) (2026-03-06) [All Commits](https://github.com/traefik/traefik/compare/v2.11.38...v2.11.40) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 8468dda025..c2a9cac68a 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.40 +# example new bugfix v2.11.41 CurrentRef = "v2.11" -PreviousRef = "v2.11.39" +PreviousRef = "v2.11.40" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.40" +FutureCurrentRefName = "v2.11.41" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000