Skip to content

Commit 9def7b5

Browse files
authored
validate_params
1 parent f314b6b commit 9def7b5

1 file changed

Lines changed: 164 additions & 1 deletion

File tree

handlers/respond/respond.go

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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).
204205
func 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

Comments
 (0)