diff --git a/internal/bodyprocessors/json.go b/internal/bodyprocessors/json.go index 5176d9369..4e9ef643b 100644 --- a/internal/bodyprocessors/json.go +++ b/internal/bodyprocessors/json.go @@ -30,12 +30,13 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.Tran // Process with recursion limit col := v.ArgsPost() data, err := readJSON(ss, bpo.RequestBodyRecursionLimit) - if err != nil { - return err - } + // The collection is populated before checking the error to still perform a best effort inspection of the payload for key, value := range data { col.SetIndex(key, 0, value) } + if err != nil { + return err + } // Store the raw JSON in the TX variable for validateSchema // This is needed because RequestBody is a Single interface without a Set method @@ -59,12 +60,13 @@ func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.Tra // Process with no recursion limit as we don't have a directive for response body col := v.ResponseArgs() data, err := readJSON(ss, ignoreJSONRecursionLimit) - if err != nil { - return err - } + // The collection is populated before checking the error to still perform a best effort inspection of the payload for key, value := range data { col.SetIndex(key, 0, value) } + if err != nil { + return err + } // Store the raw JSON in the TX variable for validateSchema // This is needed because ResponseBody is a Single interface without a Set method @@ -80,12 +82,15 @@ func readJSON(s string, maxRecursion int) (map[string]string, error) { res := make(map[string]string) key := []byte("json") + json := gjson.Parse(s) + err := readItems(json, key, maxRecursion, res) + if err != nil { + return res, err + } if !gjson.Valid(s) { return res, errors.New("invalid JSON") } - json := gjson.Parse(s) - err := readItems(json, key, maxRecursion, res) - return res, err + return res, nil } // Transform JSON to a map[string]string diff --git a/testing/engine/json.go b/testing/engine/json.go index 512b9507e..e10d678de 100644 --- a/testing/engine/json.go +++ b/testing/engine/json.go @@ -74,3 +74,88 @@ SecRule ARGS:json.test3 "@eq 3" "id: 1010, phase:2, log, block" SecRule RESPONSE_ARGS:json.test4 "@eq 3" "id: 1011, phase:4, log, block" `, }) + +var _ = profile.RegisterProfile(profile.Profile{ + Meta: profile.Meta{ + Author: "kabbohus", + Description: "Test if truncated JSON request/response body work", + Enabled: true, + Name: "truncatedjsonyaml", + }, + Tests: []profile.Test{ + { + Title: "json", + Stages: []profile.Stage{ + { + Stage: profile.SubStage{ + Input: profile.StageInput{ + URI: "/index.php?json.test=456", + Method: "POST", + Headers: map[string]string{ + "content-type": "application/json", + }, + Data: `{"test":123, "test2": 456, "test3": [22, 44, 55]}`, + }, + Output: profile.ExpectedOutput{ + TriggeredRules: []int{ + 100, + 101, + 1100, + 1101, + 1104, + 1010, + 1000, + 1011, + 1111, + 1200, + 1201, + }, + NonTriggeredRules: []int{ + 103, + 1102, + 1103, + 1202, + 1203, + }, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Data: `{"test": 123, "test2": 456, "test3": [22, 44, 55], "test4": 3}`, + }, + }, + }, + }, + }, + }, + Rules: ` +SecRequestBodyAccess On +SecResponseBodyAccess On +SecResponseBodyMimeType application/json +SecRequestBodyLimitAction ProcessPartial +SecRequestBodyLimit 40 +SecRule REQUEST_HEADERS:content-type "application/json" "id: 100, phase:1, pass, log, ctl:requestBodyProcessor=JSON" +SecRule RESPONSE_HEADERS:content-type "application/json" "id:1000, phase:3, pass, log, ctl:responseBodyProcessor=JSON" +SecRule REQBODY_PROCESSOR "JSON" "id: 101,phase:2,log,block" + +SecRule REQBODY_ERROR "!@eq 0" "id:1111, phase:2, log, block" + +SecRule REQUEST_BODY "456" "id:103, phase:2, log" +SecRule ARGS:json.test "@eq 123" "id:1100, phase:2, log, block" +SecRule ARGS:json.test3.0 "@eq 22" "id:1101, phase:2, log, block" +SecRule ARGS:json.test3.1 "@eq 44" "id:1102, phase:2, log, block" +SecRule ARGS:json.test3.2 "@eq 55" "id:1103, phase:2, log, block" + +# Both GET and POST can be matched for the same key +SecRule ARGS:json.test "@eq 456" "id:1104, phase:2, log, block" + +SecRule ARGS:json.test3 "@eq 1" "id: 1010, phase:2, log, block" + +# Check ARGS_POST +SecRule ARGS_POST:json.test "@eq 123" "id:1200, phase:2, log, block" +SecRule ARGS_POST:json.test3.0 "@eq 22" "id:1201, phase:2, log, block" +SecRule ARGS_POST:json.test3.1 "@eq 44" "id:1202, phase:2, log, block" +SecRule ARGS_POST:json.test3.2 "@eq 55" "id:1203, phase:2, log, block" + +SecRule RESPONSE_ARGS:json.test4 "@eq 3" "id: 1011, phase:4, log, block" +`, +})