@@ -12,6 +12,7 @@ import ( "github.com/tidwall/gjson" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
Show 5 hidden lines
+ "github.com/corazawaf/coraza/v3/internal/collections" ) type jsonBodyProcessor struct{}@@ -29,11 +30,14 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.Tran
ss := s.String() // Process with recursion limit col := v.ArgsPost()- data, err := readJSON(ss, bpo.RequestBodyRecursionLimit)+ data, truncated, err := readJSON(ss, bpo.RequestBodyRecursionLimit, bpo.ArgumentLimit) // 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)
Show 17 hidden lines
}+ if truncated {+ v.ArgumentsLimitReached().(*collections.Single).Set("1")+ } if err != nil { return err }@@ -50,7 +54,7 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.Tran const ignoreJSONRecursionLimit = -1 -func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.TransactionVariables, _ plugintypes.BodyProcessorOptions) error {+func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.TransactionVariables, bpo plugintypes.BodyProcessorOptions) error { // Read the entire body to store it and process it s := strings.Builder{} if _, err := io.Copy(&s, reader); err != nil {@@ -59,11 +63,14 @@ func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.Tra
ss := s.String() // Process with no recursion limit as we don't have a directive for response body col := v.ResponseArgs()- data, err := readJSON(ss, ignoreJSONRecursionLimit)+ data, truncated, err := readJSON(ss, ignoreJSONRecursionLimit, bpo.ArgumentLimit) // 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)
Show 8 hidden lines
}+ if truncated {+ v.ArgumentsLimitReached().(*collections.Single).Set("1")+ } if err != nil { return err }@@ -78,19 +85,54 @@ func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.Tra
return nil } -func readJSON(s string, maxRecursion int) (map[string]string, error) {- res := make(map[string]string)+// readJSON flattens s into a map[string]string, stopping once argumentLimit+// entries have been collected (argumentLimit <= 0 means no limit). Without+// this, a small body decoding to a wide flat structure (e.g. a JSON array of+// millions of scalars) grows this map -- and, through it, ARGS_POST/
Show 16 hidden lines
+// RESPONSE_ARGS -- without bound regardless of SecArgumentsLimit, exhausting+// memory on a single request. See GHSA-3ww9-vw83-9w5x.+// flattenBytesFactor bounds the flattened form relative to the body that+// produced it. SecArgumentsLimit counts entries, not bytes, and the flattened+// key is the full path rewritten for every leaf, so memory grows with+// depth x leaves while the entry count grows only with leaves. A body that+// stays comfortably under the argument limit can therefore still retain many+// times its own size: a 34 KB body of long paths measured 1.6 MB of keys at+// 999 arguments, roughly 48x, with no limit reached and no flag set.+//+// The factor is a judgement call rather than a derived number. Eight leaves+// room for genuinely nested documents -- measured well under 2x on ordinary+// API payloads -- while stopping the amplification well before it matters.+const flattenBytesFactor = 8++// flattenBytesFloor keeps small bodies unaffected, where a few long keys can
+// legitimately outweigh the input.+const flattenBytesFloor = 4096++func readJSON(s string, maxRecursion int, argumentLimit int) (res map[string]string, truncated bool, err error) {+ res = make(map[string]string) key := []byte("json") + byteBudget := len(s) * flattenBytesFactor+ if byteBudget < flattenBytesFloor {+ byteBudget = flattenBytesFloor+ }++ // The walk runs before the validity check on purpose: a body with a valid
Show 2 hidden lines
+ // prefix still populates ARGS on a best-effort basis, which rules can act+ // on even though an error is returned (see the best-effort tests, and
The AI-coauthored readJSON change adds argument and byte-budget truncation; when a limit stops the flattening walk early, a deep tail is left for gjson.Valid to recurse into without the depth check.