Add maxResponseBodySize configuration to forwardAuth middleware

This commit is contained in:
Gina A. 2026-02-23 11:30:06 +01:00 committed by GitHub
parent 288e4e2e2b
commit 4595c7a920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 244 additions and 5 deletions

View File

@ -637,4 +637,65 @@ http:
[http.middlewares.test-auth.forwardAuth.tls]
insecureSkipVerify: true
```
### `maxResponseBodySize`
_Optional, Default=-1_
The `maxResponseBodySize` option defines the maximum allowed response body size in bytes from the authentication server.
If the response body exceeds the configured limit, the request is rejected with a 401 (Unauthorized) status.
If left unset, the request body size is unrestricted which can have performance or security implications.
```yaml tab="Docker"
labels:
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-auth
spec:
forwardAuth:
address: https://example.com/auth
maxResponseBodySize: 10000
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize": "10000"
}
```
```yaml tab="Rancher"
labels:
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-auth:
forwardAuth:
address: "https://example.com/auth"
maxResponseBodySize: 10000
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-auth.forwardAuth]
address = "https://example.com/auth"
maxResponseBodySize = 10000
```
!!! warning
It is strongly recommended to set this option to a suitable value.
Not setting it (or setting it to `-1`) allows unlimited response body sizes which can lead to DoS attacks and memory exhaustion.
{% include-markdown "includes/traefik-for-business-applications.md" %}

View File

@ -764,3 +764,14 @@ in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3).
Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation
for more details.
## v2.11.38
### `maxResponseBodySize` configuration on ForwardAuth middleware
In `v2.11.38`, a new `maxResponseBodySize` option has been added to the ForwardAuth middleware 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 and security issues,
such as DoS attacks and memory exhaustion.
Please check out the [ForwardAuth](../middlewares/http/forwardauth.md#maxresponsebodysize) middleware documentation for more details.

View File

@ -32,6 +32,7 @@
- "traefik.http.middlewares.middleware10.forwardauth.authrequestheaders=foobar, foobar"
- "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders=foobar, foobar"
- "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex=foobar"
- "traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize=42"
- "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar"
- "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional=true"
- "traefik.http.middlewares.middleware10.forwardauth.tls.cert=foobar"

View File

@ -155,6 +155,7 @@
authResponseHeaders = ["foobar", "foobar"]
authResponseHeadersRegex = "foobar"
authRequestHeaders = ["foobar", "foobar"]
maxResponseBodySize = 42
[http.middlewares.Middleware10.forwardAuth.tls]
ca = "foobar"
caOptional = true

View File

@ -176,6 +176,7 @@ http:
authRequestHeaders:
- foobar
- foobar
maxResponseBodySize: 42
Middleware11:
headers:
customRequestHeaders:

View File

@ -1001,6 +1001,11 @@ spec:
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
type: string
maxResponseBodySize:
description: MaxResponseBodySize defines the maximum body size
in bytes allowed in the response from the authentication server.
format: int64
type: integer
tls:
description: TLS defines the configuration used to secure the
connection to the authentication server.

View File

@ -43,6 +43,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/0` | `foobar` |
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/1` | `foobar` |
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeadersRegex` | `foobar` |
| `traefik/http/middlewares/Middleware10/forwardAuth/maxResponseBodySize` | `42` |
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` |
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/caOptional` | `true` |
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/cert` | `foobar` |

View File

@ -32,6 +32,7 @@
"traefik.http.middlewares.middleware10.forwardauth.authrequestheaders": "foobar, foobar",
"traefik.http.middlewares.middleware10.forwardauth.authresponseheaders": "foobar, foobar",
"traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex": "foobar",
"traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize": "42",
"traefik.http.middlewares.middleware10.forwardauth.tls.ca": "foobar",
"traefik.http.middlewares.middleware10.forwardauth.tls.caoptional": "true",
"traefik.http.middlewares.middleware10.forwardauth.tls.cert": "foobar",

View File

@ -386,6 +386,11 @@ spec:
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
type: string
maxResponseBodySize:
description: MaxResponseBodySize defines the maximum body size
in bytes allowed in the response from the authentication server.
format: int64
type: integer
tls:
description: TLS defines the configuration used to secure the
connection to the authentication server.

View File

@ -1001,6 +1001,11 @@ spec:
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
type: string
maxResponseBodySize:
description: MaxResponseBodySize defines the maximum body size
in bytes allowed in the response from the authentication server.
format: int64
type: integer
tls:
description: TLS defines the configuration used to secure the
connection to the authentication server.

View File

@ -216,6 +216,8 @@ type ForwardAuth struct {
// AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server.
// If not set or empty then all request headers are passed.
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"`
// MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server.
MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty" toml:"maxResponseBodySize,omitempty" yaml:"maxResponseBodySize,omitempty" export:"true"`
}
// +k8s:deepcopy-gen=true

View File

@ -324,6 +324,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.MaxResponseBodySize != nil {
in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize
*out = new(int64)
**out = **in
}
return
}

View File

@ -50,6 +50,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true",
"traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar",
"traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true",
"traefik.http.middlewares.Middleware7.forwardauth.maxresponsebodysize": "42",
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true",
"traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar",
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar",
@ -547,6 +548,7 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
MaxResponseBodySize: pointer[int64](42),
},
},
"Middleware8": {
@ -1060,6 +1062,7 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
MaxResponseBodySize: pointer[int64](42),
},
},
"Middleware8": {
@ -1259,6 +1262,7 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true",
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar",
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true",
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.MaxResponseBodySize": "42",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT",

View File

@ -47,11 +47,13 @@ type forwardAuth struct {
client http.Client
trustForwardHeader bool
authRequestHeaders []string
maxResponseBodySize int64
}
// NewForward creates a forward auth middleware.
func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) {
log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName)).Debug("Creating middleware")
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName))
logger.Debug("Creating middleware")
fa := &forwardAuth{
address: config.Address,
@ -62,6 +64,13 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
authRequestHeaders: config.AuthRequestHeaders,
}
if config.MaxResponseBodySize != nil {
fa.maxResponseBodySize = *config.MaxResponseBodySize
} else {
fa.maxResponseBodySize = -1
logger.Warn("ForwardAuth 'maxResponseBodySize' is not configured, allowing unlimited response body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
}
// Ensure our request client does not follow redirects
fa.client = http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
@ -125,9 +134,16 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
defer forwardResponse.Body.Close()
body, readError := io.ReadAll(forwardResponse.Body)
body, readError := fa.readResponseBodyBytes(forwardResponse)
if readError != nil {
logger.Debugf("Error reading body %s. Cause: %s", fa.address, readError)
if errors.Is(readError, errResponseBodyTooLarge) {
logger.Debugf("Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize)
tracing.SetErrorWithEvent(req, "Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize)
rw.WriteHeader(http.StatusUnauthorized)
return
}
logger.Debugf("Error reading body %s", fa.address)
tracing.SetErrorWithEvent(req, "Error reading body %s. Cause: %s", fa.address, readError)
rw.WriteHeader(http.StatusInternalServerError)
@ -193,6 +209,27 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
fa.next.ServeHTTP(rw, req)
}
var errResponseBodyTooLarge = errors.New("response body too large")
func (fa *forwardAuth) readResponseBodyBytes(res *http.Response) ([]byte, error) {
if fa.maxResponseBodySize < 0 {
return io.ReadAll(res.Body)
}
body := make([]byte, fa.maxResponseBodySize+1)
n, err := io.ReadFull(res.Body, body)
if errors.Is(err, io.EOF) {
return nil, nil
}
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, fmt.Errorf("reading response body bytes: %w", err)
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return body[:n], nil
}
return nil, errResponseBodyTooLarge
}
func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) {
utils.CopyHeaders(forwardReq.Header, req.Header)

View File

@ -482,6 +482,89 @@ func TestForwardAuthUsesTracing(t *testing.T) {
assert.Equal(t, http.StatusOK, res.StatusCode)
}
func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
testCases := []struct {
name string
maxResponseBodySize int64
status int
body string
expectedStatus int
expectedBody string
}{
{
name: "auth failure, unlimited response body",
maxResponseBodySize: -1,
status: http.StatusForbidden,
body: "Forbidden",
expectedStatus: http.StatusForbidden,
expectedBody: "Forbidden",
},
{
name: "auth failure, response body exceeds the limit",
maxResponseBodySize: 1,
status: http.StatusForbidden,
body: "Forbidden",
expectedStatus: http.StatusUnauthorized,
expectedBody: "",
},
{
name: "auth success within limit",
maxResponseBodySize: 100,
status: http.StatusOK,
body: "ok",
expectedStatus: http.StatusOK,
expectedBody: "traefik\n",
},
{
name: "auth success body exceeds limit",
maxResponseBodySize: 1,
status: http.StatusOK,
body: "large auth response",
expectedStatus: http.StatusUnauthorized,
expectedBody: "",
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.status)
fmt.Fprint(w, test.body)
}))
t.Cleanup(server.Close)
next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
}))
maxResponseBodySize := test.maxResponseBodySize
auth := dynamic.ForwardAuth{
Address: server.URL,
MaxResponseBodySize: &maxResponseBodySize,
}
middleware, err := NewForward(t.Context(), next, auth, "maxResponseBodySizeTest")
require.NoError(t, err)
ts := httptest.NewServer(middleware)
t.Cleanup(ts.Close)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, test.expectedStatus, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
err = res.Body.Close()
require.NoError(t, err)
assert.Equal(t, test.expectedBody, string(body))
})
}
}
type mockBackend struct {
opentracing.Tracer
}

View File

@ -652,6 +652,10 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
AuthRequestHeaders: auth.AuthRequestHeaders,
}
if auth.MaxResponseBodySize != nil {
forwardAuth.MaxResponseBodySize = auth.MaxResponseBodySize
}
if auth.TLS == nil {
return forwardAuth, nil
}

View File

@ -155,6 +155,8 @@ type ForwardAuth struct {
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
// TLS defines the configuration used to secure the connection to the authentication server.
TLS *ClientTLS `json:"tls,omitempty"`
// MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server.
MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty"`
}
// ClientTLS holds the client TLS configuration.

View File

@ -215,6 +215,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = new(ClientTLS)
**out = **in
}
if in.MaxResponseBodySize != nil {
in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize
*out = new(int64)
**out = **in
}
return
}

View File

@ -83,6 +83,7 @@ func Test_buildConfiguration(t *testing.T) {
"traefik/http/middlewares/Middleware08/forwardAuth/tls/cert": "foobar",
"traefik/http/middlewares/Middleware08/forwardAuth/address": "foobar",
"traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true",
"traefik/http/middlewares/Middleware08/forwardAuth/maxResponseBodySize": "42",
"traefik/http/middlewares/Middleware15/redirectScheme/scheme": "foobar",
"traefik/http/middlewares/Middleware15/redirectScheme/port": "foobar",
"traefik/http/middlewares/Middleware15/redirectScheme/permanent": "true",
@ -427,6 +428,7 @@ func Test_buildConfiguration(t *testing.T) {
"foobar",
"foobar",
},
MaxResponseBodySize: pointer[int64](42),
},
},
"Middleware06": {

View File

@ -287,6 +287,7 @@ func init() {
AuthResponseHeaders: []string{"foo"},
AuthResponseHeadersRegex: "foo",
AuthRequestHeaders: []string{"foo"},
MaxResponseBodySize: pointer[int64](42),
},
InFlightReq: &dynamic.InFlightReq{
Amount: 42,

View File

@ -247,7 +247,8 @@
"authResponseHeadersRegex": "foo",
"authRequestHeaders": [
"foo"
]
],
"maxResponseBodySize": 42
},
"inFlightReq": {
"amount": 42,

View File

@ -250,7 +250,8 @@
"authResponseHeadersRegex": "foo",
"authRequestHeaders": [
"foo"
]
],
"maxResponseBodySize": 42
},
"inFlightReq": {
"amount": 42,