VYPR
High severity7.5NVD Advisory· Published Feb 26, 2026· Updated Apr 14, 2026

CVE-2026-27896

CVE-2026-27896

Description

The Go MCP SDK used Go's standard encoding/json.Unmarshal for JSON-RPC and MCP protocol message parsing in versions prior to 1.3.1. Go's standard library performs case-insensitive matching of JSON keys to struct field tags — a field tagged json:"method" would also match "Method", "METHOD", etc. This violated the JSON-RPC 2.0 specification, which defines exact field names. A malicious MCP peer may have been able to send protocol messages with non-standard field casing that the SDK would silently accept. This had the potential for bypassing intermediary inspection and coss-implementation inconsistency. Go's standard JSON unmarshaling was replaced with a case-sensitive decoder in commit 7b8d81c. Users are advised to update to v1.3.1 to resolve this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/modelcontextprotocol/go-sdkGo
< 1.3.11.3.1

Affected products

1

Patches

1
7b8d81c26407

all: use case-sensitive JSON unmarshaling (#807)

https://github.com/modelcontextprotocol/go-sdkMaciej KisielFeb 18, 2026via ghsa
16 files changed · +134 25
  • go.mod+6 0 modified
    @@ -6,7 +6,13 @@ require (
     	github.com/golang-jwt/jwt/v5 v5.3.0
     	github.com/google/go-cmp v0.7.0
     	github.com/google/jsonschema-go v0.4.2
    +	github.com/segmentio/encoding v0.5.3
     	github.com/yosida95/uritemplate/v3 v3.0.2
     	golang.org/x/oauth2 v0.34.0
     	golang.org/x/tools v0.41.0
     )
    +
    +require (
    +	github.com/segmentio/asm v1.1.3 // indirect
    +	golang.org/x/sys v0.40.0 // indirect
    +)
    
  • go.sum+6 0 modified
    @@ -4,9 +4,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
     github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
     github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
     github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
    +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
    +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
    +github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
    +github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
     github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
     github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
     golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
     golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
    +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
    +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
     golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
     golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
    
  • internal/json/json.go+19 0 added
    @@ -0,0 +1,19 @@
    +// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
    +// Use of this source code is governed by the license
    +// that can be found in the LICENSE file.
    +
    +// Package json provides internal JSON utilities.
    +
    +package json
    +
    +import (
    +	"bytes"
    +
    +	"github.com/segmentio/encoding/json"
    +)
    +
    +func Unmarshal(data []byte, v any) error {
    +	dec := json.NewDecoder(bytes.NewReader(data))
    +	dec.DontMatchCaseInsensitiveStructFields()
    +	return dec.Decode(v)
    +}
    
  • internal/json/json_test.go+63 0 added
    @@ -0,0 +1,63 @@
    +// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
    +// Use of this source code is governed by the license
    +// that can be found in the LICENSE file.
    +
    +package json
    +
    +import (
    +	"testing"
    +
    +	"github.com/google/go-cmp/cmp"
    +)
    +
    +func TestUnmarshalCaseSensitivity(t *testing.T) {
    +	type Nested struct {
    +		Field string `json:"field"`
    +	}
    +	type Target struct {
    +		Field       string
    +		TaggedField string `json:"custom_tag"`
    +		Nested      *Nested
    +	}
    +
    +	tests := []struct {
    +		name string
    +		json string
    +		want Target
    +	}{
    +		{
    +			name: "exact match",
    +			json: `{"Field": "value", "custom_tag": "tagged", "Nested": {"field": "nested"}}`,
    +			want: Target{
    +				Field:       "value",
    +				TaggedField: "tagged",
    +				Nested: &Nested{
    +					Field: "nested",
    +				},
    +			},
    +		},
    +		{
    +			name: "case mismatch",
    +			json: `{"field": "value", "Custom_tag": "tagged", "Nested": {"Field": "nested"}}`,
    +			want: Target{
    +				Field:       "",
    +				TaggedField: "",
    +				Nested: &Nested{
    +					Field: "",
    +				},
    +			},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			var got Target
    +			if err := Unmarshal([]byte(tt.json), &got); err != nil {
    +				t.Fatalf("Unmarshal failed: %v", err)
    +			}
    +			if diff := cmp.Diff(tt.want, got); diff != "" {
    +				t.Errorf("Unmarshal mismatch (-want +got):\n%s", diff)
    +			}
    +		})
    +	}
    +}
    
  • internal/jsonrpc2/conn.go+2 1 modified
    @@ -6,13 +6,14 @@ package jsonrpc2
     
     import (
     	"context"
    -	"encoding/json"
     	"errors"
     	"fmt"
     	"io"
     	"sync"
     	"sync/atomic"
     	"time"
    +
    +	"github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     // Binder builds a connection configuration.
    
  • internal/jsonrpc2/messages.go+3 1 modified
    @@ -10,6 +10,8 @@ import (
     	"errors"
     	"fmt"
     
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
    +
     	"github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug"
     )
     
    @@ -173,7 +175,7 @@ func EncodeIndent(msg Message, prefix, indent string) ([]byte, error) {
     
     func DecodeMessage(data []byte) (Message, error) {
     	msg := wireCombined{}
    -	if err := json.Unmarshal(data, &msg); err != nil {
    +	if err := internaljson.Unmarshal(data, &msg); err != nil {
     		return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err)
     	}
     	if msg.VersionTag != wireVersion {
    
  • mcp/client.go+1 2 modified
    @@ -6,7 +6,6 @@ package mcp
     
     import (
     	"context"
    -	"encoding/json"
     	"errors"
     	"fmt"
     	"iter"
    @@ -18,7 +17,7 @@ import (
     	"time"
     
     	"github.com/google/jsonschema-go/jsonschema"
    -
    +	"github.com/modelcontextprotocol/go-sdk/internal/json"
     	"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
     	"github.com/modelcontextprotocol/go-sdk/jsonrpc"
     )
    
  • mcp/content.go+5 3 modified
    @@ -10,6 +10,8 @@ package mcp
     import (
     	"encoding/json"
     	"fmt"
    +
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     // A Content is a [TextContent], [ImageContent], [AudioContent],
    @@ -248,7 +250,7 @@ func (c *ToolResultContent) MarshalJSON() ([]byte, error) {
     			return nil, err
     		}
     		var w wireContent
    -		if err := json.Unmarshal(data, &w); err != nil {
    +		if err := internaljson.Unmarshal(data, &w); err != nil {
     			return nil, err
     		}
     		contentWire = append(contentWire, &w)
    @@ -328,11 +330,11 @@ func unmarshalContent(raw json.RawMessage, allow map[string]bool) ([]Content, er
     	}
     	// Try array first, then fall back to single object.
     	var wires []*wireContent
    -	if err := json.Unmarshal(raw, &wires); err == nil {
    +	if err := internaljson.Unmarshal(raw, &wires); err == nil {
     		return contentsFromWire(wires, allow)
     	}
     	var wire wireContent
    -	if err := json.Unmarshal(raw, &wire); err != nil {
    +	if err := internaljson.Unmarshal(raw, &wire); err != nil {
     		return nil, err
     	}
     	c, err := contentFromWire(&wire, allow)
    
  • mcp/protocol.go+9 7 modified
    @@ -14,6 +14,8 @@ import (
     	"encoding/json"
     	"fmt"
     	"maps"
    +
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     // Optional annotations for the client. The client can use annotations to inform
    @@ -141,7 +143,7 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error {
     		res
     		Content []*wireContent `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	var err error
    @@ -315,7 +317,7 @@ type CompleteReference struct {
     func (r *CompleteReference) UnmarshalJSON(data []byte) error {
     	type wireCompleteReference CompleteReference // for naive unmarshaling
     	var r2 wireCompleteReference
    -	if err := json.Unmarshal(data, &r2); err != nil {
    +	if err := internaljson.Unmarshal(data, &r2); err != nil {
     		return err
     	}
     	switch r2.Type {
    @@ -502,7 +504,7 @@ func (m *SamplingMessageV2) UnmarshalJSON(data []byte) error {
     		msg
     		Content json.RawMessage `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	var err error
    @@ -542,7 +544,7 @@ func (r *CreateMessageResult) UnmarshalJSON(data []byte) error {
     		result
     		Content *wireContent `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	var err error
    @@ -605,7 +607,7 @@ func (r *CreateMessageWithToolsResult) UnmarshalJSON(data []byte) error {
     		result
     		Content json.RawMessage `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	var err error
    @@ -1056,7 +1058,7 @@ func (m *PromptMessage) UnmarshalJSON(data []byte) error {
     		msg
     		Content *wireContent `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	var err error
    @@ -1253,7 +1255,7 @@ func (m *SamplingMessage) UnmarshalJSON(data []byte) error {
     		msg
     		Content *wireContent `json:"content"`
     	}
    -	if err := json.Unmarshal(data, &wire); err != nil {
    +	if err := internaljson.Unmarshal(data, &wire); err != nil {
     		return err
     	}
     	// Allow text, image, audio, tool_use, and tool_result in sampling messages
    
  • mcp/server.go+3 2 modified
    @@ -25,6 +25,7 @@ import (
     	"time"
     
     	"github.com/google/jsonschema-go/jsonschema"
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     	"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
     	"github.com/modelcontextprotocol/go-sdk/internal/util"
     	"github.com/modelcontextprotocol/go-sdk/jsonrpc"
    @@ -327,7 +328,7 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa
     		// Unmarshal and validate args.
     		var in In
     		if input != nil {
    -			if err := json.Unmarshal(input, &in); err != nil {
    +			if err := internaljson.Unmarshal(input, &in); err != nil {
     				return nil, fmt.Errorf("%w: %v", jsonrpc2.ErrInvalidParams, err)
     			}
     		}
    @@ -1371,7 +1372,7 @@ func initializeMethodInfo() methodInfo {
     	info.unmarshalParams = func(m json.RawMessage) (Params, error) {
     		var params *initializeParamsV2
     		if m != nil {
    -			if err := json.Unmarshal(m, &params); err != nil {
    +			if err := internaljson.Unmarshal(m, &params); err != nil {
     				return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, params, err)
     			}
     		}
    
  • mcp/shared.go+2 1 modified
    @@ -23,6 +23,7 @@ import (
     	"time"
     
     	"github.com/modelcontextprotocol/go-sdk/auth"
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     	"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
     	"github.com/modelcontextprotocol/go-sdk/jsonrpc"
     )
    @@ -283,7 +284,7 @@ func newMethodInfo[P paramsPtr[T], R Result, T any](flags methodFlags) methodInf
     		unmarshalParams: func(m json.RawMessage) (Params, error) {
     			var p P
     			if m != nil {
    -				if err := json.Unmarshal(m, &p); err != nil {
    +				if err := internaljson.Unmarshal(m, &p); err != nil {
     					return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, p, err)
     				}
     			}
    
  • mcp/streamable.go+2 1 modified
    @@ -30,6 +30,7 @@ import (
     	"time"
     
     	"github.com/modelcontextprotocol/go-sdk/auth"
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     	"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
     	"github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug"
     	"github.com/modelcontextprotocol/go-sdk/internal/util"
    @@ -1105,7 +1106,7 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques
     				isInitialize = true
     				// Extract the protocol version from InitializeParams.
     				var params InitializeParams
    -				if err := json.Unmarshal(jreq.Params, &params); err == nil {
    +				if err := internaljson.Unmarshal(jreq.Params, &params); err == nil {
     					initializeProtocolVersion = params.ProtocolVersion
     				}
     			}
    
  • mcp/tool.go+2 1 modified
    @@ -11,6 +11,7 @@ import (
     	"strings"
     
     	"github.com/google/jsonschema-go/jsonschema"
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     // A ToolHandler handles a call to tools/call.
    @@ -83,7 +84,7 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawM
     	if resolved != nil {
     		v := make(map[string]any)
     		if len(data) > 0 {
    -			if err := json.Unmarshal(data, &v); err != nil {
    +			if err := internaljson.Unmarshal(data, &v); err != nil {
     				return nil, fmt.Errorf("unmarshaling arguments: %w", err)
     			}
     		}
    
  • mcp/transport.go+3 2 modified
    @@ -15,6 +15,7 @@ import (
     	"os"
     	"sync"
     
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     	"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
     	"github.com/modelcontextprotocol/go-sdk/internal/xcontext"
     	"github.com/modelcontextprotocol/go-sdk/jsonrpc"
    @@ -193,7 +194,7 @@ type canceller struct {
     func (c *canceller) Preempt(ctx context.Context, req *jsonrpc.Request) (result any, err error) {
     	if req.Method == notificationCancelled {
     		var params CancelledParams
    -		if err := json.Unmarshal(req.Params, &params); err != nil {
    +		if err := internaljson.Unmarshal(req.Params, &params); err != nil {
     			return nil, err
     		}
     		id, err := jsonrpc2.MakeID(params.RequestID)
    @@ -569,7 +570,7 @@ func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) {
     func readBatch(data []byte) (msgs []jsonrpc.Message, isBatch bool, _ error) {
     	// Try to read an array of messages first.
     	var rawBatch []json.RawMessage
    -	if err := json.Unmarshal(data, &rawBatch); err == nil {
    +	if err := internaljson.Unmarshal(data, &rawBatch); err == nil {
     		if len(rawBatch) == 0 {
     			return nil, true, fmt.Errorf("empty batch")
     		}
    
  • mcp/util.go+3 1 modified
    @@ -6,6 +6,8 @@ package mcp
     
     import (
     	"encoding/json"
    +
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     func assert(cond bool, msg string) {
    @@ -21,7 +23,7 @@ func remarshal(from, to any) error {
     	if err != nil {
     		return err
     	}
    -	if err := json.Unmarshal(data, to); err != nil {
    +	if err := internaljson.Unmarshal(data, to); err != nil {
     		return err
     	}
     	return nil
    
  • oauthex/dcr.go+5 3 modified
    @@ -17,6 +17,8 @@ import (
     	"io"
     	"net/http"
     	"time"
    +
    +	internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
     )
     
     // ClientRegistrationMetadata represents the client metadata fields for the DCR POST request (RFC 7591).
    @@ -144,7 +146,7 @@ func (r *ClientRegistrationResponse) UnmarshalJSON(data []byte) error {
     	}{
     		alias: (*alias)(r),
     	}
    -	if err := json.Unmarshal(data, &aux); err != nil {
    +	if err := internaljson.Unmarshal(data, &aux); err != nil {
     		return err
     	}
     	if aux.ClientIDIssuedAt != 0 {
    @@ -206,7 +208,7 @@ func RegisterClient(ctx context.Context, registrationEndpoint string, clientMeta
     
     	if resp.StatusCode == http.StatusCreated {
     		var regResponse ClientRegistrationResponse
    -		if err := json.Unmarshal(body, &regResponse); err != nil {
    +		if err := internaljson.Unmarshal(body, &regResponse); err != nil {
     			return nil, fmt.Errorf("failed to decode successful registration response: %w (%s)", err, string(body))
     		}
     		if regResponse.ClientID == "" {
    @@ -221,7 +223,7 @@ func RegisterClient(ctx context.Context, registrationEndpoint string, clientMeta
     
     	if resp.StatusCode == http.StatusBadRequest {
     		var regError ClientRegistrationError
    -		if err := json.Unmarshal(body, &regError); err != nil {
    +		if err := internaljson.Unmarshal(body, &regError); err != nil {
     			return nil, fmt.Errorf("failed to decode registration error response: %w (%s)", err, string(body))
     		}
     		return nil, &regError
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.