Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions pkg/acquisition/modules/appsec/appsec_bodysize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package appsecacquisition

import (
"net/http"
"net/url"
"testing"

"github.com/stretchr/testify/require"

"github.com/crowdsecurity/crowdsec/pkg/appsec"
"github.com/crowdsecurity/crowdsec/pkg/appsec/appsec_rule"
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
)

func TestAppsecBodySize(t *testing.T) {
tests := []appsecRuleTest{
{
// Same pattern as pre_eval DropRequest: 3 events (APPSEC + LOG inband + LOG outband)
// because BodySizeExceeded triggers DropRequest in both inband and outband processRequest.
name: "body size exceeded – default ban",
expected_load_ok: true,
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
HTTPRequest: &http.Request{Host: "example.com"},
BodySizeExceeded: true,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, appsec.BanRemediation, responses[0].Action)
require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
require.Len(t, events, 3)
require.Equal(t, pipeline.APPSEC, events[0].Type)
require.Equal(t, pipeline.LOG, events[1].Type)
require.Equal(t, pipeline.LOG, events[2].Type)
require.True(t, events[1].Appsec.HasInBandMatches)
require.True(t, events[2].Appsec.HasOutBandMatches)
require.Equal(t, "request body exceeded maximum allowed size", events[1].Parsed["appsec_drop_reason"])
},
},
{
name: "body size exceeded – on_match changes status code",
expected_load_ok: true,
on_match: []appsec.Hook{
{Filter: "IsInBand == true", Apply: []string{"SetReturnCode(413)"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
HTTPRequest: &http.Request{Host: "example.com"},
BodySizeExceeded: true,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, 413, responses[0].UserHTTPResponseCode)
require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
},
},
{
name: "body size exceeded – on_match cancels inband alert and event",
expected_load_ok: true,
on_match: []appsec.Hook{
{Filter: "IsInBand == true", Apply: []string{"CancelAlert()", "CancelEvent()"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
HTTPRequest: &http.Request{Host: "example.com"},
BodySizeExceeded: true,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
// Inband alert+event canceled; outband LOG event still fires
require.Len(t, events, 1)
require.Equal(t, pipeline.LOG, events[0].Type)
require.True(t, events[0].Appsec.HasOutBandMatches)
},
},
{
// Body was truncated to the limit; the matched content is in the kept portion.
name: "body truncated (partial) – rule matches on kept content",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"BODY_ARGS"},
Variables: []string{"payload"},
Match: appsec_rule.Match{Type: "contains", Value: "MALICIOUS"},
},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
Headers: http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
HTTPRequest: &http.Request{Host: "example.com"},
Body: []byte("payload=MALICIOUS"),
BodyTruncated: true,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, appsec.BanRemediation, responses[0].Action)
},
},
{
// Body was truncated; the rule matches content only present in the discarded tail.
name: "body truncated (partial) – rule misses content beyond truncation point",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"BODY_ARGS"},
Variables: []string{"payload"},
Match: appsec_rule.Match{Type: "contains", Value: "DANGER"},
},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
Headers: http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
HTTPRequest: &http.Request{Host: "example.com"},
Body: []byte("payload=safe"),
BodyTruncated: true,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.False(t, responses[0].InBandInterrupt)
},
},
{
// Body is nil (allow action): body rules do not fire.
name: "body nil (allow action) – body rule does not fire",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"BODY_ARGS"},
Variables: []string{"payload"},
Match: appsec_rule.Match{Type: "contains", Value: "TRIGGER"},
},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
Headers: http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
HTTPRequest: &http.Request{Host: "example.com"},
Body: nil,
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.False(t, responses[0].InBandInterrupt)
},
},
}

runTests(t, tests)
}

func TestAppsecDisableBodyInspection(t *testing.T) {
tests := []appsecRuleTest{
{
name: "DisableBodyInspection - body rule does not fire",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"BODY_ARGS"},
Variables: []string{"payload"},
Match: appsec_rule.Match{Type: "contains", Value: "MALICIOUS"},
},
},
pre_eval: []appsec.Hook{
{Filter: "1 == 1", Apply: []string{"DisableBodyInspection()"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/",
Headers: http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
HTTPRequest: &http.Request{Host: "example.com"},
Body: []byte("payload=MALICIOUS"),
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.False(t, responses[0].InBandInterrupt)
require.Empty(t, events)
},
},
{
name: "DisableBodyInspection - ARGS rule still fires (phase 2 still evaluated)",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"ARGS"},
Variables: []string{"foo"},
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
},
},
pre_eval: []appsec.Hook{
{Filter: "1 == 1", Apply: []string{"DisableBodyInspection()"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "GET",
URI: "/?foo=toto",
Args: url.Values{"foo": []string{"toto"}},
HTTPRequest: &http.Request{Host: "example.com"},
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, appsec.BanRemediation, responses[0].Action)
},
},
{
name: "DisableBodyInspection - conditional filter, body inspected when filter does not match",
expected_load_ok: true,
inband_rules: []appsec_rule.CustomRule{
{
Name: "rule1",
Zones: []string{"BODY_ARGS"},
Variables: []string{"payload"},
Match: appsec_rule.Match{Type: "contains", Value: "MALICIOUS"},
},
},
pre_eval: []appsec.Hook{
{Filter: "req.URL.Path startsWith '/upload'", Apply: []string{"DisableBodyInspection()"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "POST",
URI: "/api",
Headers: http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
HTTPRequest: &http.Request{Host: "example.com"},
Body: []byte("payload=MALICIOUS"),
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, appsec.BanRemediation, responses[0].Action)
},
},
}

runTests(t, tests)
}
37 changes: 26 additions & 11 deletions pkg/acquisition/modules/appsec/appsec_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ func (r *AppsecRunner) processRequest(state *appsec.AppsecRequestState, request
return nil
}

if request.BodySizeExceeded {
r.logger.Warnf("request body exceeded maximum allowed size, dropping request")
if err = r.AppsecRuntime.DropRequest(state, request, "request body exceeded maximum allowed size"); err != nil {
r.logger.Errorf("unable to drop request: %s", err)
}
return nil
}

state.Tx.ProcessConnection(request.ClientIP, 0, "", 0)

for k, v := range request.Args {
Expand Down Expand Up @@ -193,21 +201,28 @@ func (r *AppsecRunner) processRequest(state *appsec.AppsecRequestState, request
return nil
}

if len(request.Body) > 0 {
in, _, err = state.Tx.WriteRequestBody(request.Body)
if err != nil {
r.logger.Errorf("unable to write request body : %s", err)
return err
if state.DisableBodyInspection {
r.logger.Debugf("body inspection is disabled for this request, skipping body write")
} else {
if request.BodyTruncated {
r.logger.Warnf("request body was truncated to %d bytes (partial mode)", len(request.Body))
}
if in != nil {
return nil

if len(request.Body) > 0 {
in, _, err = state.Tx.WriteRequestBody(request.Body)
if err != nil {
r.logger.Warnf("unable to write request body, skipping body inspection: %s", err)
} else if in != nil {
return nil
}
}
}

in, err = state.Tx.ProcessRequestBody()
if err != nil {
r.logger.Errorf("unable to process request body : %s", err)
return err
if err == nil {
in, err = state.Tx.ProcessRequestBody()
if err != nil {
r.logger.Warnf("unable to process request body, skipping body inspection: %s", err)
}
}

if in != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/acquisition/modules/appsec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func (w *Source) appsecHandler(rw http.ResponseWriter, r *http.Request) {
}

// parse the request only once
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger)
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger, w.AppsecRuntime.BodySettings)
if err != nil {
w.logger.Errorf("%s", err)
rw.WriteHeader(http.StatusInternalServerError)
Expand Down
Loading
Loading