Prevent client to send more than one JSON's object. Prevent client to send unknown fields. Prevent client to send more than 1MB of data per endpoint, preventing DDOS attacks.

This commit is contained in:
Maxime Delporte
2025-10-27 19:45:37 +01:00
parent c3fdb40ae4
commit 599ab6b3c6

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
/* /*
@@ -86,13 +87,23 @@ func (app *application) writeJSON(w http.ResponseWriter, status int, data envelo
} }
func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any) error { func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any) error {
// Initialize a new json.Decoder instance which reads from the request body, and then use the Decode() method to decode the body contents into the input struct. // Use the http.MaxBytesReader() to limit the size of the request body to 1MB.
err := json.NewDecoder(r.Body).Decode(dst) maxBytes := 1_048_576
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
// Initialize the json.Decoder, and call the DisallowUnknownFields() method on it before decoding. This means that if the JSON from the client now includes any field which cannot be mapped to the target destination, the target will return an error instead of just ignoring the field.
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
// Decode the request body to the destination
err := dec.Decode(dst)
if err != nil { if err != nil {
// If there is an error during decoding, start the triage... // If there is an error during decoding, start the triage...
var syntaxError *json.SyntaxError var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError var invalidUnmarshalError *json.InvalidUnmarshalError
// Add a new maxBytesError variable
var maxBytesError *http.MaxBytesError
switch { switch {
// Use the errors.As() function to check whether the error has the type *json.SyntaxError. If it does, then return a plain-english error message which includes the location problem. // Use the errors.As() function to check whether the error has the type *json.SyntaxError. If it does, then return a plain-english error message which includes the location problem.
@@ -114,16 +125,31 @@ func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any
case errors.Is(err, io.EOF): case errors.Is(err, io.EOF):
return errors.New("body must not be empty") return errors.New("body must not be empty")
// If the JSON contains a field which cannot be mapped to the target destination then Decode() will now return an error message in the format "json: unknown field "<name>"". We check for this, extract the field name from the error, and interpolate it into our custom error message. Note that there's an open issue at https://github.com/golang/go/issues/29035 regarding turning this into a distinct error type in the future.
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("body contains unknown key %s", fieldName)
// Use the errors.As() function to check whether the error has the type *http.MaxBytesError. If it does, then it means the request body exceeded our size limit of 1MB, and we return a clear error message.
case errors.As(err, &maxBytesError):
return fmt.Errorf("body must not be larger than %d bytes", maxBytesError.Limit)
// A json.InvalidUnmarshalError error will be returned if we pass something that is not a non-nil pointer to Decode(). We catch this and panic, rather than returning an error to our handler. // A json.InvalidUnmarshalError error will be returned if we pass something that is not a non-nil pointer to Decode(). We catch this and panic, rather than returning an error to our handler.
// This is firmly an unexpected error which we shouldn't see under normal operation, and is something that should be picked up in development and tests long before deployment. // This is firmly an unexpected error which we shouldn't see under normal operation, and is something that should be picked up in development and tests long before deployment.
case errors.As(err, &invalidUnmarshalError): case errors.As(err, &invalidUnmarshalError):
panic(err) panic(err)
// For anything else, return the error mesasge as-is. // For anything else, return the error message as-is.
default: default:
return err return err
} }
} }
// Call Decode() again, using a pointer to an empty anonymous struct as the destination. If the request body only contained a single JSON value this will return an io.EOF error. So if we get anything else, we know that there is additional data in the request body, and we return our own custom error message.
err = dec.Decode(&struct{}{})
if !errors.Is(err, io.EOF) {
return errors.New("body must only contain a single JSON value")
}
return nil return nil
} }