@@ -20,6 +20,7 @@ import (
2020 "fmt"
2121 "io"
2222 "net/http"
23+ "reflect"
2324 "strings"
2425
2526 "github.com/go-chi/chi/v5"
@@ -202,15 +203,177 @@ func DecodeBody[T interface {
202203// structs that have no Validate method; client-native models embedded in such
203204// a body should still be validated individually (see Unprocessable).
204205func DecodeJSON (r * http.Request , w http.ResponseWriter , out any ) bool {
205- dec := json .NewDecoder (r .Body )
206+ body , err := io .ReadAll (r .Body )
207+ if err != nil {
208+ BadRequest (w , err .Error ())
209+ return false
210+ }
211+
212+ if err := strictUnknownFieldsCheck (body , out ); err != nil {
213+ log .Warningf ("STRICT JSON UNKNOWN FIELD ERROR: %v" , err )
214+ BadRequest (w , err .Error ())
215+ return false
216+ }
217+
218+ dec := json .NewDecoder (bytes .NewReader (body ))
206219 dec .UseNumber ()
207220 if err := dec .Decode (out ); err != nil {
208221 BadRequest (w , err .Error ())
209222 return false
210223 }
224+
211225 return true
212226}
213227
228+ func strictUnknownFieldsCheck (body []byte , target any ) error {
229+ if len (bytes .TrimSpace (body )) == 0 {
230+ return nil
231+ }
232+
233+ // Raw/plain config endpoint decodes body into string.
234+ // Do not apply JSON strict check to string targets.
235+ if _ , ok := target .(* string ); ok {
236+ return nil
237+ }
238+
239+ var raw any
240+
241+ dec := json .NewDecoder (bytes .NewReader (body ))
242+ dec .UseNumber ()
243+
244+ if err := dec .Decode (& raw ); err != nil {
245+ return err
246+ }
247+
248+ return checkUnknownFields (raw , reflect .TypeOf (target ), "" )
249+ }
250+
251+ func checkUnknownFields (raw any , targetType reflect.Type , path string ) error {
252+ if targetType == nil {
253+ return nil
254+ }
255+
256+ for targetType .Kind () == reflect .Pointer {
257+ targetType = targetType .Elem ()
258+ }
259+
260+ switch rawValue := raw .(type ) {
261+ case map [string ]any :
262+ switch targetType .Kind () {
263+ case reflect .Struct :
264+ allowed := jsonFieldNames (targetType )
265+
266+ for key , value := range rawValue {
267+ fieldType , ok := allowed [key ]
268+ if ! ok {
269+ if path == "" {
270+ return fmt .Errorf ("unknown field %q" , key )
271+ }
272+
273+ return fmt .Errorf ("unknown field %q at %s" , key , path )
274+ }
275+
276+ childPath := key
277+ if path != "" {
278+ childPath = path + "." + key
279+ }
280+
281+ if err := checkUnknownFields (value , fieldType , childPath ); err != nil {
282+ return err
283+ }
284+ }
285+
286+ case reflect .Map :
287+ elemType := targetType .Elem ()
288+
289+ for key , value := range rawValue {
290+ childPath := key
291+ if path != "" {
292+ childPath = path + "." + key
293+ }
294+
295+ if err := checkUnknownFields (value , elemType , childPath ); err != nil {
296+ return err
297+ }
298+ }
299+ }
300+
301+ case []any :
302+ elemType := targetType
303+
304+ for elemType .Kind () == reflect .Pointer {
305+ elemType = elemType .Elem ()
306+ }
307+
308+ if elemType .Kind () == reflect .Slice || elemType .Kind () == reflect .Array {
309+ elemType = elemType .Elem ()
310+ }
311+
312+ for index , item := range rawValue {
313+ childPath := fmt .Sprintf ("%s[%d]" , path , index )
314+
315+ if err := checkUnknownFields (item , elemType , childPath ); err != nil {
316+ return err
317+ }
318+ }
319+ }
320+
321+ return nil
322+ }
323+
324+ func jsonFieldNames (t reflect.Type ) map [string ]reflect.Type {
325+ fields := make (map [string ]reflect.Type )
326+
327+ for t .Kind () == reflect .Pointer {
328+ t = t .Elem ()
329+ }
330+
331+ if t .Kind () != reflect .Struct {
332+ return fields
333+ }
334+
335+ for i := 0 ; i < t .NumField (); i ++ {
336+ field := t .Field (i )
337+
338+ // Skip unexported non-embedded fields.
339+ if field .PkgPath != "" && ! field .Anonymous {
340+ continue
341+ }
342+
343+ fieldType := field .Type
344+ for fieldType .Kind () == reflect .Pointer {
345+ fieldType = fieldType .Elem ()
346+ }
347+
348+ tag := field .Tag .Get ("json" )
349+ name := strings .Split (tag , "," )[0 ]
350+
351+ if tag == "-" {
352+ continue
353+ }
354+
355+ // Embedded struct without explicit json name:
356+ // type Resolver struct { ResolverBase }
357+ // Need to flatten ResolverBase fields into parent object.
358+ if field .Anonymous && (name == "" || name == field .Name ) {
359+ if fieldType .Kind () == reflect .Struct {
360+ for embeddedName , embeddedType := range jsonFieldNames (fieldType ) {
361+ fields [embeddedName ] = embeddedType
362+ }
363+ continue
364+ }
365+ }
366+
367+ if name == "" {
368+ name = field .Name
369+ }
370+
371+ fields [name ] = field .Type
372+ }
373+
374+ return fields
375+ }
376+
214377// Unprocessable writes a 422 Unprocessable Entity response with a cleaned
215378// message from a go-openapi CompositeError. The body code matches the HTTP
216379// status, consistent with the rest of the API.
0 commit comments