Skip to content

Commit bf893e0

Browse files
authored
feat: limit body buffering to avoid resource exhaustion (#8)
* Limit body buffering to avoid resource exhaustion * Add documentation and examples for maxBodySize parameters
1 parent 19cdb47 commit bf893e0

6 files changed

+62
-3
lines changed

.traefik.yml

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ summary: 'Traefik plugin to proxy requests to owasp/modsecurity-crs:apache'
77

88
testData:
99
ModsecurityUrl: http://waf:80
10+
MaxBodySize: 10485760
1011

1112
iconPath: ./img/icon.png
1213
bannerPath: ./img/banner.png

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ If it is > 400, then the error page is returned instead.
4040

4141
The *dummy* service is created so the waf container forward the request to a service and respond with 200 OK all the time.
4242

43+
## Configuration
44+
45+
This plugin supports these configuration:
46+
47+
* `modSecurityUrl`: (**mandatory**) it's the URL for the owasp/modsecurity container.
48+
* `maxBodySize`: (optional) it's the maximum limit for requests body size. Requests exceeding this value will be rejected using `HTTP 413 Request Entity Too Large`.
49+
The default value for this parameter is 10MB. Zero means "use default value".
50+
51+
**Note**: body of every request will be buffered in memory while the request is in-flight (i.e.: during the security check and during the request processing by traefik and the backend), so you may want to tune `maxBodySize` depending on how much RAM you have.
4352

4453
## Local development (docker-compose.local.yml)
4554

docker-compose.local.yml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ services:
2222
- traefik.enable=true
2323
- traefik.http.services.traefik.loadbalancer.server.port=8080
2424
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.modSecurityUrl=http://waf:80
25+
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.maxBodySize=10485760
2526

2627
waf:
2728
image: owasp/modsecurity-crs:apache

docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ services:
2222
- traefik.enable=true
2323
- traefik.http.services.traefik.loadbalancer.server.port=8080
2424
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.modSecurityUrl=http://waf:80
25+
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.maxBodySize=10485760
2526

2627
waf:
2728
image: owasp/modsecurity-crs:apache

modsecurity.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var httpClient = &http.Client{
2121
// Config the plugin configuration.
2222
type Config struct {
2323
ModSecurityUrl string `json:"modSecurityUrl,omitempty"`
24+
MaxBodySize int64 `json:"maxBodySize,omitempty"`
2425
}
2526

2627
// CreateConfig creates the default plugin configuration.
@@ -32,6 +33,7 @@ func CreateConfig() *Config {
3233
type Modsecurity struct {
3334
next http.Handler
3435
modSecurityUrl string
36+
maxBodySize int64
3537
name string
3638
logger *log.Logger
3739
}
@@ -42,6 +44,13 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
4244
return nil, fmt.Errorf("modSecurityUrl cannot be empty")
4345
}
4446

47+
// Safe default: if the max body size was not specified, use 10MB
48+
// Note that this will break any file upload with files > 10MB. Hopefully
49+
// the user will configure this parameter during the installation.
50+
if config.MaxBodySize == 0 {
51+
config.MaxBodySize = 10 * 1024 * 1024
52+
}
53+
4554
return &Modsecurity{
4655
modSecurityUrl: config.ModSecurityUrl,
4756
next: next,
@@ -60,10 +69,15 @@ func (a *Modsecurity) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
6069

6170
// we need to buffer the body if we want to read it here and send it
6271
// in the request.
63-
body, err := ioutil.ReadAll(req.Body)
72+
body, err := ioutil.ReadAll(http.MaxBytesReader(rw, req.Body, a.maxBodySize))
6473
if err != nil {
65-
a.logger.Printf("fail to read incoming request: %s", err.Error())
66-
http.Error(rw, "", http.StatusBadGateway)
74+
if err.Error() == "http: request body too large" {
75+
a.logger.Printf("body max limit reached: %s", err.Error())
76+
http.Error(rw, "", http.StatusRequestEntityTooLarge)
77+
} else {
78+
a.logger.Printf("fail to read incoming request: %s", err.Error())
79+
http.Error(rw, "", http.StatusBadGateway)
80+
}
6781
return
6882
}
6983

modsecurity_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ func TestModsecurity_ServeHTTP(t *testing.T) {
7676
expectBody: "Response from service",
7777
expectStatus: 200,
7878
},
79+
{
80+
name: "Accept payloads smaller than limits",
81+
request: http.Request{
82+
Body: generateLargeBody(1024),
83+
},
84+
wafResponse: response{
85+
StatusCode: 200,
86+
Body: "Response from waf",
87+
},
88+
serviceResponse: serviceResponse,
89+
expectBody: "Response from service",
90+
expectStatus: http.StatusOK,
91+
},
92+
{
93+
name: "Reject too big payloads",
94+
request: http.Request{
95+
Body: generateLargeBody(1025),
96+
},
97+
wafResponse: response{
98+
StatusCode: 200,
99+
Body: "Response from waf",
100+
},
101+
serviceResponse: serviceResponse,
102+
expectBody: "\n",
103+
expectStatus: http.StatusRequestEntityTooLarge,
104+
},
79105
}
80106
for _, tt := range tests {
81107
t.Run(tt.name, func(t *testing.T) {
@@ -101,7 +127,9 @@ func TestModsecurity_ServeHTTP(t *testing.T) {
101127
middleware := &Modsecurity{
102128
next: httpServiceHandler,
103129
modSecurityUrl: modsecurityMockServer.URL,
130+
maxBodySize: 1024,
104131
name: "modsecurity-middleware",
132+
logger: log.New(io.Discard, "", log.LstdFlags),
105133
}
106134

107135
rw := httptest.NewRecorder()
@@ -116,3 +144,8 @@ func TestModsecurity_ServeHTTP(t *testing.T) {
116144
})
117145
}
118146
}
147+
148+
func generateLargeBody(size int) io.ReadCloser {
149+
var str = make([]byte, size)
150+
return io.NopCloser(bytes.NewReader(str))
151+
}

0 commit comments

Comments
 (0)