VYPR
High severity7.1NVD Advisory· Published Mar 24, 2026· Updated Apr 15, 2026

CVE-2026-33252

CVE-2026-33252

Description

The Go MCP SDK used Go's standard encoding/json. Prior to version 1.4.1, the Go SDK's Streamable HTTP transport accepted browser-generated cross-site POST requests without validating the Origin header and without requiring Content-Type: application/json. In deployments without Authorization, especially stateless or sessionless configurations, this allows an arbitrary website to send MCP requests to a local server and potentially trigger tool execution. Version 1.4.1 contains a patch for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

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

Affected products

1

Patches

1
a433a831d6e5

mcp: verify 'Origin' and 'Content-Type' headers (#842)

https://github.com/modelcontextprotocol/go-sdkMaciej KisielMar 13, 2026via ghsa
15 files changed · +252 11
  • docs/mcpgodebug.md+41 0 added
    @@ -0,0 +1,41 @@
    +<!-- Autogenerated by weave; DO NOT EDIT -->
    + # Backwards compatibility and MCPGODEBUG
    +
    + According to our compatibility promise, we can't break backward compatibility
    + of the SDK API. However, sometimes we need to change the behavior of the SDK
    + in a backward-incompatible way in order to fix bugs or security issues.
    + In those cases we introduce temporary compatibility parameters, that can be
    + used to opt-out of the new behavior. They are usually maintained for two
    + minor release cycles and then removed.
    +
    + The compatibility parameters are provided via the `MCPGODEBUG` environment
    + variable. The value of the variable is a comma-separated list of parameter
    + value assignments, e.g.:
    +
    + ```
    + MCPGODEBUG=parameter1=value1,parameter2=value2
    + ```
    +
    +## `MCPGODEBUG` history
    +
    +### 1.4.1
    +
    +Options listed below will be removed in the 1.6.0 version of the SDK.
    +
    +- `disablecrossoriginprotection` added. If set to `1`, newly added cross-origin
    +  protection will be disabled. The default behavior was changed to enable
    +  cross-origin protection.
    +
    +### 1.4.0
    +
    +Options listed below will be removed in the 1.6.0 version of the SDK.
    +
    +- `jsonescaping` added. If set to `1`, JSON marshaling will preserve the previous
    +  behavior of escaping HTML characters in JSON strings. The default behavior
    +  was changed to not escape HTML characters, to be consistent with other SDKs.
    +
    +- `disablelocalhostprotection` added. If set to `1`, newly added DNS rebinding
    +  protection will be disabled. The default behavior was changed to enable DNS rebinding
    +  protection. The protection can also be disabled by setting the
    +  `DisableLocalhostProtection` field in the `StreamableHTTPOptions` struct to
    +  `true`, which is the recommended way to disable the protection long term.
    
  • docs/protocol.md+1 1 modified
    @@ -360,7 +360,7 @@ and step-up authentication (when the server returns `insufficient_scope` error).
     ## Security
     
     Here we discuss the mitigations described under
    -the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) section, and how we handle them.
    +the MCP's [Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) section, and how we handle them.
     
     ### Confused Deputy
     
    
  • docs/README.md+5 0 modified
    @@ -39,6 +39,11 @@ protocol.
     
     See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
     
    +# Backwards compatibility
    +
    +See [mcpgodebug.md](mcpgodebug.md) for a list of backwards incompatible behavior changes
    +and description how they can be temporarily undone.
    +
     # Rough edges
     
     See [rough_edges.md](rough_edges.md) for a list of rough edges or API
    
  • .github/workflows/codeql.yml+3 0 modified
    @@ -8,6 +8,9 @@ on:
       schedule:
         - cron: '31 9 * * 4'
     
    +# Declare default permissions as read only.
    +permissions: read-all
    +
     jobs:
       analyze:
         name: Analyze (${{ matrix.language }})
    
  • .github/workflows/conformance.yml+2 2 modified
    @@ -22,7 +22,7 @@ jobs:
           - name: Set up Go
             uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
             with:
    -          go-version: "^1.25"
    +          go-version: "^1.26"
           - name: Start everything-server
             run: |
               go run ./conformance/everything-server/main.go -http=":3001" &
    @@ -45,7 +45,7 @@ jobs:
           - name: Set up Go
             uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
             with:
    -          go-version: "^1.25"
    +          go-version: "^1.26"
           - name: "Run conformance tests" 
             uses: modelcontextprotocol/conformance@a2855b03582a6c0b31065ad4d9af248316ce61a3 # v0.1.15
             with:
    
  • .github/workflows/docs-check.yml+2 0 modified
    @@ -17,6 +17,8 @@ jobs:
         steps:
           - name: Set up Go
             uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
    +        with:
    +          go-version: "^1.26"
           - name: Check out code
             uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
           - name: Check docs are up-to-date
    
  • .github/workflows/nightly.yml+1 1 modified
    @@ -27,7 +27,7 @@ jobs:
         - name: Set up Go
           uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
           with:
    -        go-version: "^1.25"
    +        go-version: "^1.26"
         - name: Set up Node.js
           uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
           with:
    
  • .github/workflows/test.yml+4 2 modified
    @@ -40,7 +40,7 @@ jobs:
         runs-on: ubuntu-latest
         strategy:
           matrix:
    -        go: ["1.24", "1.25"]
    +        go: ["1.25", "1.26"]
         steps:
           - name: Check out code
             uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    @@ -52,13 +52,15 @@ jobs:
             run: go test -v ./...
     
       race-test:
    +    # Temporarily disable until fixes are prepared.
    +    if: false
         runs-on: ubuntu-latest
         steps:
           - name: Check out code
             uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
           - name: Set up Go
             uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
             with:
    -          go-version: "1.24"
    +          go-version: "1.25"
           - name: Test with -race
             run: go test -v -race ./...
    
  • go.mod+1 1 modified
    @@ -1,6 +1,6 @@
     module github.com/modelcontextprotocol/go-sdk
     
    -go 1.24.0
    +go 1.25.0
     
     require (
     	github.com/golang-jwt/jwt/v5 v5.3.1
    
  • internal/docs/doc.go+1 0 modified
    @@ -9,6 +9,7 @@
     //go:generate weave -o ../../docs/server.md ./server.src.md
     //go:generate weave -o ../../docs/troubleshooting.md ./troubleshooting.src.md
     //go:generate weave -o ../../docs/rough_edges.md ./rough_edges.src.md
    +//go:generate weave -o ../../docs/mcpgodebug.md ./mcpgodebug.src.md
     
     // The doc package generates the documentation at /doc, via go:generate.
     //
    
  • internal/docs/mcpgodebug.src.md+40 0 added
    @@ -0,0 +1,40 @@
    + # Backwards compatibility and MCPGODEBUG
    +
    + According to our compatibility promise, we can't break backward compatibility
    + of the SDK API. However, sometimes we need to change the behavior of the SDK
    + in a backward-incompatible way in order to fix bugs or security issues.
    + In those cases we introduce temporary compatibility parameters, that can be
    + used to opt-out of the new behavior. They are usually maintained for two
    + minor release cycles and then removed.
    +
    + The compatibility parameters are provided via the `MCPGODEBUG` environment
    + variable. The value of the variable is a comma-separated list of parameter
    + value assignments, e.g.:
    +
    + ```
    + MCPGODEBUG=parameter1=value1,parameter2=value2
    + ```
    +
    +## `MCPGODEBUG` history
    +
    +### 1.4.1
    +
    +Options listed below will be removed in the 1.6.0 version of the SDK.
    +
    +- `disablecrossoriginprotection` added. If set to `1`, newly added cross-origin
    +  protection will be disabled. The default behavior was changed to enable
    +  cross-origin protection.
    +
    +### 1.4.0
    +
    +Options listed below will be removed in the 1.6.0 version of the SDK.
    +
    +- `jsonescaping` added. If set to `1`, JSON marshaling will preserve the previous
    +  behavior of escaping HTML characters in JSON strings. The default behavior
    +  was changed to not escape HTML characters, to be consistent with other SDKs.
    +
    +- `disablelocalhostprotection` added. If set to `1`, newly added DNS rebinding
    +  protection will be disabled. The default behavior was changed to enable DNS rebinding
    +  protection. The protection can also be disabled by setting the
    +  `DisableLocalhostProtection` field in the `StreamableHTTPOptions` struct to
    +  `true`, which is the recommended way to disable the protection long term.
    \ No newline at end of file
    
  • internal/docs/protocol.src.md+1 1 modified
    @@ -285,7 +285,7 @@ and step-up authentication (when the server returns `insufficient_scope` error).
     ## Security
     
     Here we discuss the mitigations described under
    -the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) section, and how we handle them.
    +the MCP's [Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) section, and how we handle them.
     
     ### Confused Deputy
     
    
  • internal/docs/README.src.md+5 0 modified
    @@ -38,6 +38,11 @@ protocol.
     
     See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
     
    +# Backwards compatibility
    +
    +See [mcpgodebug.md](mcpgodebug.md) for a list of backwards incompatible behavior changes
    +and description how they can be temporarily undone.
    +
     # Rough edges
     
     See [rough_edges.md](rough_edges.md) for a list of rough edges or API
    
  • mcp/streamable.go+35 2 modified
    @@ -174,6 +174,14 @@ type StreamableHTTPOptions struct {
     	// Only disable this if you understand the security implications.
     	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
     	DisableLocalhostProtection bool
    +
    +	// CrossOriginProtection allows to customize cross-origin protection.
    +	// The deny handler set in the CrossOriginProtection through SetDenyHandler
    +	// is ignored.
    +	// If nil, default (zero-value) cross-origin protection will be used.
    +	// Use `disablecrossoriginprotection` MCPGODEBUG compatibility parameter
    +	// to disable the default protection until v1.6.0.
    +	CrossOriginProtection *http.CrossOriginProtection
     }
     
     // NewStreamableHTTPHandler returns a new [StreamableHTTPHandler].
    @@ -190,8 +198,10 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea
     		h.opts = *opts
     	}
     
    -	if h.opts.Logger == nil { // ensure we have a logger
    -		h.opts.Logger = ensureLogger(nil)
    +	h.opts.Logger = ensureLogger(h.opts.Logger)
    +
    +	if h.opts.CrossOriginProtection == nil {
    +		h.opts.CrossOriginProtection = &http.CrossOriginProtection{}
     	}
     
     	return h
    @@ -226,6 +236,13 @@ func (h *StreamableHTTPHandler) closeAll() {
     // The option will be removed in the 1.6.0 version of the SDK.
     var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")
     
    +// disablecrossoriginprotection is a compatibility parameter that allows to disable
    +// the verification of the 'Origin' and 'Content-Type' headers, which was added in
    +// the 1.4.1 version of the SDK. See the documentation for the mcpgodebug package
    +// for instructions how to enable it.
    +// The option will be removed in the 1.6.0 version of the SDK.
    +var disablecrossoriginprotection = mcpgodebug.Value("disablecrossoriginprotection")
    +
     func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
     	// DNS rebinding protection: auto-enabled for localhost servers.
     	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
    @@ -238,6 +255,22 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
     		}
     	}
     
    +	if disablecrossoriginprotection != "1" {
    +		// Verify the 'Origin' header to protect against CSRF attacks.
    +		if err := h.opts.CrossOriginProtection.Check(req); err != nil {
    +			http.Error(w, err.Error(), http.StatusForbidden)
    +			return
    +		}
    +		// Validate 'Content-Type' header.
    +		if req.Method == http.MethodPost {
    +			contentType := req.Header.Get("Content-Type")
    +			if contentType != "application/json" {
    +				http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType)
    +				return
    +			}
    +		}
    +	}
    +
     	// Allow multiple 'Accept' headers.
     	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
     	accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
    
  • mcp/streamable_test.go+110 1 modified
    @@ -832,6 +832,36 @@ func TestStreamableServerTransport(t *testing.T) {
     			},
     			wantSessions: 0,
     		},
    +		{
    +			name: "content type headers",
    +			requests: []streamableRequest{
    +				initialize,
    +				initialized,
    +				{
    +					// Request with incorrect Content-Type should be rejected.
    +					method:         "POST",
    +					headers:        http.Header{"Content-Type": {"text/plain"}},
    +					messages:       []jsonrpc.Message{req(3, "tools/call", &CallToolParams{Name: "tool"})},
    +					wantStatusCode: http.StatusUnsupportedMediaType,
    +				},
    +				{
    +					// Request with empty Content-Type should be rejected.
    +					method:         "POST",
    +					headers:        http.Header{"Content-Type": {""}},
    +					messages:       []jsonrpc.Message{req(4, "tools/call", &CallToolParams{Name: "tool"})},
    +					wantStatusCode: http.StatusUnsupportedMediaType,
    +				},
    +				{
    +					// Correct Content-Type should pass.
    +					method:         "POST",
    +					headers:        http.Header{"Content-Type": {"application/json"}},
    +					messages:       []jsonrpc.Message{req(5, "tools/call", &CallToolParams{Name: "tool"})},
    +					wantStatusCode: http.StatusOK,
    +					wantMessages:   []jsonrpc.Message{resp(5, &CallToolResult{Content: []Content{}}, nil)},
    +				},
    +			},
    +			wantSessions: 1,
    +		},
     		{
     			name: "accept headers",
     			requests: []streamableRequest{
    @@ -1409,10 +1439,16 @@ func (s streamableRequest) do(ctx context.Context, serverURL, sessionID string,
     	if sessionID != "" {
     		req.Header.Set(sessionIDHeader, sessionID)
     	}
    -	req.Header.Set("Content-Type", "application/json")
    +	if s.method == http.MethodPost {
    +		req.Header.Set("Content-Type", "application/json")
    +	}
     	req.Header.Set("Accept", "application/json, text/event-stream")
     	maps.Copy(req.Header, s.headers)
     
    +	if req.Header.Get("Content-Type") == "" {
    +		req.Header.Del("Content-Type")
    +	}
    +
     	resp, err := http.DefaultClient.Do(req)
     	if err != nil {
     		return "", 0, nil, fmt.Errorf("request failed: %v", err)
    @@ -2436,3 +2472,76 @@ func TestStreamableLocalhostProtection(t *testing.T) {
     		})
     	}
     }
    +
    +func TestStreamableOriginProtection(t *testing.T) {
    +	server := NewServer(testImpl, nil)
    +
    +	tests := []struct {
    +		name           string
    +		protection     *http.CrossOriginProtection
    +		requestOrigin  string
    +		wantStatusCode int
    +	}{
    +		{
    +			name:           "default protection with Origin header",
    +			protection:     nil,
    +			requestOrigin:  "https://example.com",
    +			wantStatusCode: http.StatusForbidden,
    +		},
    +		{
    +			name: "custom protection with trusted origin and same Origin",
    +			protection: func() *http.CrossOriginProtection {
    +				p := http.NewCrossOriginProtection()
    +				if err := p.AddTrustedOrigin("https://example.com"); err != nil {
    +					t.Fatal(err)
    +				}
    +				return p
    +			}(),
    +			requestOrigin:  "https://example.com",
    +			wantStatusCode: http.StatusOK,
    +		},
    +		{
    +			name: "custom protection with trusted origin and different Origin",
    +			protection: func() *http.CrossOriginProtection {
    +				p := http.NewCrossOriginProtection()
    +				if err := p.AddTrustedOrigin("https://example.com"); err != nil {
    +					t.Fatal(err)
    +				}
    +				return p
    +			}(),
    +			requestOrigin:  "https://malicious.com",
    +			wantStatusCode: http.StatusForbidden,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			opts := &StreamableHTTPOptions{
    +				Stateless:             true, // avoid session ID requirement
    +				CrossOriginProtection: tt.protection,
    +			}
    +			handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, opts)
    +			httpServer := httptest.NewServer(handler)
    +			defer httpServer.Close()
    +
    +			reqReader := strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}`)
    +			req, err := http.NewRequest(http.MethodPost, httpServer.URL, reqReader)
    +			if err != nil {
    +				t.Fatal(err)
    +			}
    +			req.Header.Set("Content-Type", "application/json")
    +			req.Header.Set("Origin", tt.requestOrigin)
    +			req.Header.Set("Accept", "application/json, text/event-stream")
    +
    +			resp, err := http.DefaultClient.Do(req)
    +			if err != nil {
    +				t.Fatal(err)
    +			}
    +			defer resp.Body.Close()
    +
    +			if got := resp.StatusCode; got != tt.wantStatusCode {
    +				t.Errorf("Status code: got %d, want %d", got, tt.wantStatusCode)
    +			}
    +		})
    +	}
    +}
    

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.