-
-
Notifications
You must be signed in to change notification settings - Fork 230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add SecRequestBodyJsonDepthLimit directive #1110
base: main
Are you sure you want to change the base?
Changes from 4 commits
337a07d
b901570
d176401
d7a5e31
ac3323d
0dd8824
67d9582
dfdba03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
package bodyprocessors | ||
|
||
import ( | ||
"errors" | ||
"io" | ||
"strconv" | ||
"strings" | ||
|
@@ -13,13 +14,15 @@ import ( | |
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" | ||
) | ||
|
||
const ResponseBodyRecursionLimit = -1 | ||
|
||
type jsonBodyProcessor struct{} | ||
|
||
var _ plugintypes.BodyProcessor = &jsonBodyProcessor{} | ||
|
||
func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.TransactionVariables, _ plugintypes.BodyProcessorOptions) error { | ||
func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.TransactionVariables, bpo plugintypes.BodyProcessorOptions) error { | ||
col := v.ArgsPost() | ||
data, err := readJSON(reader) | ||
data, err := readJSON(reader, bpo.RequestBodyRecursionLimit) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -31,7 +34,7 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.Tran | |
|
||
func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.TransactionVariables, _ plugintypes.BodyProcessorOptions) error { | ||
col := v.ResponseArgs() | ||
data, err := readJSON(reader) | ||
data, err := readJSON(reader, ResponseBodyRecursionLimit) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -41,28 +44,38 @@ func (js *jsonBodyProcessor) ProcessResponse(reader io.Reader, v plugintypes.Tra | |
return nil | ||
} | ||
|
||
func readJSON(reader io.Reader) (map[string]string, error) { | ||
func readJSON(reader io.Reader, maxRecursion int) (map[string]string, error) { | ||
s := strings.Builder{} | ||
_, err := io.Copy(&s, reader) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
json := gjson.Parse(s.String()) | ||
res := make(map[string]string) | ||
key := []byte("json") | ||
readItems(json, key, res) | ||
return res, nil | ||
|
||
if !gjson.Valid(s.String()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we really need to do this? the first impression is this is very expensive, the second one is when we use a low body limit this will always be invalid isn't/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not very expensive. Of course, it is more expensive than not doing vlaidation (adds roughly 33% more time in my benchmarks). The upstream library says that If you are consuming JSON from an unpredictable source then you may want to validate prior to using GJSON. I agree, for the case of a WAF. Or we can forget about validation, and see if someone can bypass us. 🤷 If we do, we should document clearly this decision that we prefer speed over security here. |
||
return res, errors.New("invalid JSON") | ||
} | ||
json := gjson.Parse(s.String()) | ||
err = readItems(json, key, maxRecursion, res) | ||
return res, err | ||
} | ||
|
||
// Transform JSON to a map[string]string | ||
// This function is recursive and will call itself for nested objects. | ||
// The limit in recursion is defined by maxItems. | ||
// Example input: {"data": {"name": "John", "age": 30}, "items": [1,2,3]} | ||
// Example output: map[string]string{"json.data.name": "John", "json.data.age": "30", "json.items.0": "1", "json.items.1": "2", "json.items.2": "3"} | ||
// Example input: [{"data": {"name": "John", "age": 30}, "items": [1,2,3]}] | ||
// Example output: map[string]string{"json.0.data.name": "John", "json.0.data.age": "30", "json.0.items.0": "1", "json.0.items.1": "2", "json.0.items.2": "3"} | ||
// TODO add some anti DOS protection | ||
func readItems(json gjson.Result, objKey []byte, res map[string]string) { | ||
func readItems(json gjson.Result, objKey []byte, maxRecursion int, res map[string]string) error { | ||
arrayLen := 0 | ||
var iterationError error | ||
if maxRecursion == 0 { | ||
// we reached the limit of nesting we want to handle | ||
return errors.New("max recursion reached while reading json object") | ||
} | ||
json.ForEach(func(key, value gjson.Result) bool { | ||
// Avoid string concatenation to maintain a single buffer for key aggregation. | ||
prevParentLength := len(objKey) | ||
|
@@ -77,7 +90,11 @@ func readItems(json gjson.Result, objKey []byte, res map[string]string) { | |
var val string | ||
switch value.Type { | ||
case gjson.JSON: | ||
readItems(value, objKey, res) | ||
// call recursively with one less item to avoid doing infinite recursion | ||
iterationError = readItems(value, objKey, maxRecursion-1, res) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
if iterationError != nil { | ||
return false | ||
} | ||
objKey = objKey[:prevParentLength] | ||
return true | ||
case gjson.String: | ||
|
@@ -97,6 +114,7 @@ func readItems(json gjson.Result, objKey []byte, res map[string]string) { | |
if arrayLen > 0 { | ||
res[string(objKey)] = strconv.Itoa(arrayLen) | ||
} | ||
return iterationError | ||
} | ||
|
||
func init() { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should never be -1 and always force a limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we are assuming we don't trust the downstream service neither?
The directive takes into account that the request might be crafted so it creates a DoS of some kind on the body processor (as it happened in ModSecurity). Will the response value be configurable, and different from the request?