mcp: add DNS rebinding and cross origin protections to SSE transport (#891)
Similar protections were already introduced before to the Streamable transport.
This commit is contained in:
parent
1209861858
commit
15e93a231c
1 changed files with 236 additions and 14 deletions
57
mcp/sse.go
57
mcp/sse.go
|
|
@ -10,11 +10,13 @@ import (
|
|||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
)
|
||||
|
||||
|
|
@ -52,9 +54,25 @@ type SSEHandler struct {
|
|||
}
|
||||
|
||||
// SSEOptions specifies options for an [SSEHandler].
|
||||
// for now, it is empty, but may be extended in future.
|
||||
// https://github.com/modelcontextprotocol/go-sdk/issues/507
|
||||
type SSEOptions struct{}
|
||||
type SSEOptions struct {
|
||||
// DisableLocalhostProtection disables automatic DNS rebinding protection.
|
||||
// By default, requests arriving via a localhost address (127.0.0.1, [::1])
|
||||
// that have a non-localhost Host header are rejected with 403 Forbidden.
|
||||
// This protects against DNS rebinding attacks regardless of whether the
|
||||
// server is listening on localhost specifically or on 0.0.0.0.
|
||||
//
|
||||
// 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.7.0.
|
||||
CrossOriginProtection *http.CrossOriginProtection
|
||||
}
|
||||
|
||||
// NewSSEHandler returns a new [SSEHandler] that creates and manages MCP
|
||||
// sessions created via incoming HTTP requests.
|
||||
|
|
@ -79,6 +97,10 @@ func NewSSEHandler(getServer func(request *http.Request) *Server, opts *SSEOptio
|
|||
s.opts = *opts
|
||||
}
|
||||
|
||||
if s.opts.CrossOriginProtection == nil {
|
||||
s.opts.CrossOriginProtection = &http.CrossOriginProtection{}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -179,9 +201,34 @@ func (t *SSEServerTransport) Connect(context.Context) (Connection, error) {
|
|||
}
|
||||
|
||||
func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
sessionID := req.URL.Query().Get("sessionid")
|
||||
// DNS rebinding protection: auto-enabled for localhost servers.
|
||||
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
|
||||
if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" {
|
||||
if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
|
||||
if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
|
||||
http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider checking Content-Type here. For now, we are lax.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionID := req.URL.Query().Get("sessionid")
|
||||
|
||||
// For POST requests, the message body is a message to send to a session.
|
||||
if req.Method == http.MethodPost {
|
||||
|
|
|
|||
Loading…
Reference in a new issue