Skip to content

Commit 6e8ca56

Browse files
committed
all: use case-sensitive JSON unmarshaling (#807)
As reported in #805, case insensitive unmarshaling can pose security issues. This change adjusts all non-test and non-example unmarshaling to use a case sensitive mode. This introduces a new external dependency `github.com/segmentio/encoding`.
1 parent 6b75899 commit 6e8ca56

15 files changed

Lines changed: 127 additions & 20 deletions

File tree

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ require (
66
github.com/golang-jwt/jwt/v5 v5.2.2
77
github.com/google/go-cmp v0.7.0
88
github.com/google/jsonschema-go v0.4.2
9+
github.com/segmentio/encoding v0.5.3
910
github.com/yosida95/uritemplate/v3 v3.0.2
1011
golang.org/x/oauth2 v0.30.0
1112
golang.org/x/tools v0.34.0
1213
)
14+
15+
require (
16+
github.com/segmentio/asm v1.1.3 // indirect
17+
golang.org/x/sys v0.35.0 // indirect
18+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
44
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
55
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
66
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
7+
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
8+
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
9+
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
10+
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
711
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
812
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
913
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
1014
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
15+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
16+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
1117
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
1218
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=

internal/json/json.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package json provides internal JSON utilities.
6+
7+
package json
8+
9+
import (
10+
"bytes"
11+
12+
"github.com/segmentio/encoding/json"
13+
)
14+
15+
func Unmarshal(data []byte, v any) error {
16+
dec := json.NewDecoder(bytes.NewReader(data))
17+
dec.DontMatchCaseInsensitiveStructFields()
18+
return dec.Decode(v)
19+
}

internal/json/json_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package json
6+
7+
import (
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
)
12+
13+
func TestUnmarshalCaseSensitivity(t *testing.T) {
14+
type Nested struct {
15+
Field string `json:"field"`
16+
}
17+
type Target struct {
18+
Field string
19+
TaggedField string `json:"custom_tag"`
20+
Nested *Nested
21+
}
22+
23+
tests := []struct {
24+
name string
25+
json string
26+
want Target
27+
}{
28+
{
29+
name: "exact match",
30+
json: `{"Field": "value", "custom_tag": "tagged", "Nested": {"field": "nested"}}`,
31+
want: Target{
32+
Field: "value",
33+
TaggedField: "tagged",
34+
Nested: &Nested{
35+
Field: "nested",
36+
},
37+
},
38+
},
39+
{
40+
name: "case mismatch",
41+
json: `{"field": "value", "Custom_tag": "tagged", "Nested": {"Field": "nested"}}`,
42+
want: Target{
43+
Field: "",
44+
TaggedField: "",
45+
Nested: &Nested{
46+
Field: "",
47+
},
48+
},
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
var got Target
55+
if err := Unmarshal([]byte(tt.json), &got); err != nil {
56+
t.Fatalf("Unmarshal failed: %v", err)
57+
}
58+
if diff := cmp.Diff(tt.want, got); diff != "" {
59+
t.Errorf("Unmarshal mismatch (-want +got):\n%s", diff)
60+
}
61+
})
62+
}
63+
}

internal/jsonrpc2/conn.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ package jsonrpc2
66

77
import (
88
"context"
9-
"encoding/json"
109
"errors"
1110
"fmt"
1211
"io"
1312
"sync"
1413
"sync/atomic"
1514
"time"
15+
16+
"github.com/modelcontextprotocol/go-sdk/internal/json"
1617
)
1718

1819
// Binder builds a connection configuration.

internal/jsonrpc2/messages.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"encoding/json"
99
"errors"
1010
"fmt"
11+
12+
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
1113
)
1214

1315
// ID is a Request identifier, which is defined by the spec to be a string, integer, or null.
@@ -167,7 +169,7 @@ func EncodeIndent(msg Message, prefix, indent string) ([]byte, error) {
167169

168170
func DecodeMessage(data []byte) (Message, error) {
169171
msg := wireCombined{}
170-
if err := json.Unmarshal(data, &msg); err != nil {
172+
if err := internaljson.Unmarshal(data, &msg); err != nil {
171173
return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err)
172174
}
173175
if msg.VersionTag != wireVersion {

mcp/client.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package mcp
66

77
import (
88
"context"
9-
"encoding/json"
109
"errors"
1110
"fmt"
1211
"iter"
@@ -18,7 +17,7 @@ import (
1817
"time"
1918

2019
"github.com/google/jsonschema-go/jsonschema"
21-
20+
"github.com/modelcontextprotocol/go-sdk/internal/json"
2221
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
2322
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2423
)

mcp/protocol.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ package mcp
1313
import (
1414
"encoding/json"
1515
"fmt"
16+
17+
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
1618
)
1719

1820
// Optional annotations for the client. The client can use annotations to inform
@@ -140,7 +142,7 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error {
140142
res
141143
Content []*wireContent `json:"content"`
142144
}
143-
if err := json.Unmarshal(data, &wire); err != nil {
145+
if err := internaljson.Unmarshal(data, &wire); err != nil {
144146
return err
145147
}
146148
var err error
@@ -286,7 +288,7 @@ type CompleteReference struct {
286288
func (r *CompleteReference) UnmarshalJSON(data []byte) error {
287289
type wireCompleteReference CompleteReference // for naive unmarshaling
288290
var r2 wireCompleteReference
289-
if err := json.Unmarshal(data, &r2); err != nil {
291+
if err := internaljson.Unmarshal(data, &r2); err != nil {
290292
return err
291293
}
292294
switch r2.Type {
@@ -402,7 +404,7 @@ func (r *CreateMessageResult) UnmarshalJSON(data []byte) error {
402404
result
403405
Content *wireContent `json:"content"`
404406
}
405-
if err := json.Unmarshal(data, &wire); err != nil {
407+
if err := internaljson.Unmarshal(data, &wire); err != nil {
406408
return err
407409
}
408410
var err error
@@ -838,7 +840,7 @@ func (m *PromptMessage) UnmarshalJSON(data []byte) error {
838840
msg
839841
Content *wireContent `json:"content"`
840842
}
841-
if err := json.Unmarshal(data, &wire); err != nil {
843+
if err := internaljson.Unmarshal(data, &wire); err != nil {
842844
return err
843845
}
844846
var err error
@@ -1014,7 +1016,7 @@ func (m *SamplingMessage) UnmarshalJSON(data []byte) error {
10141016
msg
10151017
Content *wireContent `json:"content"`
10161018
}
1017-
if err := json.Unmarshal(data, &wire); err != nil {
1019+
if err := internaljson.Unmarshal(data, &wire); err != nil {
10181020
return err
10191021
}
10201022
var err error

mcp/server.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"time"
2525

2626
"github.com/google/jsonschema-go/jsonschema"
27+
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
2728
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
2829
"github.com/modelcontextprotocol/go-sdk/internal/util"
2930
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
@@ -326,7 +327,7 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa
326327
// Unmarshal and validate args.
327328
var in In
328329
if input != nil {
329-
if err := json.Unmarshal(input, &in); err != nil {
330+
if err := internaljson.Unmarshal(input, &in); err != nil {
330331
return nil, fmt.Errorf("%w: %v", jsonrpc2.ErrInvalidParams, err)
331332
}
332333
}
@@ -1325,7 +1326,7 @@ func initializeMethodInfo() methodInfo {
13251326
info.unmarshalParams = func(m json.RawMessage) (Params, error) {
13261327
var params *initializeParamsV2
13271328
if m != nil {
1328-
if err := json.Unmarshal(m, &params); err != nil {
1329+
if err := internaljson.Unmarshal(m, &params); err != nil {
13291330
return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, params, err)
13301331
}
13311332
}

mcp/shared.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"time"
2424

2525
"github.com/modelcontextprotocol/go-sdk/auth"
26+
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
2627
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
2728
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2829
)
@@ -283,7 +284,7 @@ func newMethodInfo[P paramsPtr[T], R Result, T any](flags methodFlags) methodInf
283284
unmarshalParams: func(m json.RawMessage) (Params, error) {
284285
var p P
285286
if m != nil {
286-
if err := json.Unmarshal(m, &p); err != nil {
287+
if err := internaljson.Unmarshal(m, &p); err != nil {
287288
return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, p, err)
288289
}
289290
}

0 commit comments

Comments
 (0)