From 651df66a6ecaa88127af971b2922c837cb022885 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Mon, 14 Sep 2026 11:11:40 -0700 Subject: [PATCH] json: reject trailing data after a payload Decoder.Decode accepts extra tokens after the first value. Use json.Unmarshal so leftover garbage fails, matching encoding/json. --- decoder.go | 11 ++++++++--- decoder_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 decoder_test.go diff --git a/decoder.go b/decoder.go index c01be83..ba436bc 100644 --- a/decoder.go +++ b/decoder.go @@ -37,10 +37,15 @@ func DefaultDecoder(r *http.Request, v interface{}) error { return err } -// DecodeJSON decodes a given reader into an interface using the json decoder. +// DecodeJSON decodes a given reader into an interface using encoding/json. +// json.Unmarshal rejects trailing data after a single value (golang/go#36225); +// Decoder.Decode does not. func DecodeJSON(r io.Reader, v interface{}) error { - defer io.Copy(io.Discard, r) //nolint:errcheck - return json.NewDecoder(r).Decode(v) + data, err := io.ReadAll(r) + if err != nil { + return err + } + return json.Unmarshal(data, v) } // DecodeXML decodes a given reader into an interface using the xml decoder. diff --git a/decoder_test.go b/decoder_test.go new file mode 100644 index 0000000..54c726a --- /dev/null +++ b/decoder_test.go @@ -0,0 +1,50 @@ +package render + +import ( + "bytes" + "net/http" + "strings" + "testing" +) + +func TestDecodeJSONRejectsTrailingData(t *testing.T) { + var v map[string]interface{} + err := DecodeJSON(strings.NewReader("{}\n some garbage data"), &v) + if err == nil { + t.Fatal("expected error for trailing garbage after JSON object") + } +} + +func TestDecodeJSONAllowsTrailingWhitespace(t *testing.T) { + var v map[string]string + if err := DecodeJSON(strings.NewReader("{\"a\":\"b\"}\n\t "), &v); err != nil { + t.Fatal(err) + } + if v["a"] != "b" { + t.Fatalf("got %#v", v) + } +} + +func TestDecodeJSONValidObject(t *testing.T) { + var v struct { + N int `json:"n"` + } + if err := DecodeJSON(bytes.NewReader([]byte(`{"n":7}`)), &v); err != nil { + t.Fatal(err) + } + if v.N != 7 { + t.Fatalf("got %d", v.N) + } +} + +func TestDefaultDecoderJSONTrailingData(t *testing.T) { + r, err := http.NewRequest(http.MethodPost, "/", strings.NewReader("{}\n extra")) + if err != nil { + t.Fatal(err) + } + r.Header.Set("Content-Type", "application/json") + var v map[string]interface{} + if err := DefaultDecoder(r, &v); err == nil { + t.Fatal("expected trailing-data error") + } +}