| 253 | var disablecontenttypecheck = mcpgodebug.Value("disablecontenttypecheck") |
| 254 | |
| 255 | func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
| 256 | // DNS rebinding protection: auto-enabled for localhost servers. |
| 257 | // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise |
| 258 | if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" { |
| 259 | if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil { |
| 260 | if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { |
| 261 | http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden) |
| 262 | return |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | if h.opts.CrossOriginProtection != nil { |
| 268 | // Verify the 'Origin' header to protect against CSRF attacks. |
| 269 | if err := h.opts.CrossOriginProtection.Check(req); err != nil { |
| 270 | http.Error(w, err.Error(), http.StatusForbidden) |
| 271 | return |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | // Validate 'Content-Type' header. |
| 276 | if disablecontenttypecheck != "1" && req.Method == http.MethodPost && baseMediaType(req.Header.Get("Content-Type")) != "application/json" { |
| 277 | http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) |
| 278 | return |
| 279 | } |
| 280 | |
| 281 | // Allow multiple 'Accept' headers. |
| 282 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax |
| 283 | jsonOK, streamOK := streamableAccepts(req.Header.Values("Accept")) |
| 284 | |
| 285 | if req.Method == http.MethodGet { |
| 286 | if !streamOK { |
| 287 | http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest) |
| 288 | return |
| 289 | } |
| 290 | } else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below. |
| 291 | http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest) |
| 292 | return |
| 293 | } |
| 294 | |
| 295 | sessionID := req.Header.Get(sessionIDHeader) |
| 296 | var sessInfo *sessionInfo |
| 297 | if sessionID != "" { |
| 298 | h.mu.Lock() |
| 299 | sessInfo = h.sessions[sessionID] |
| 300 | h.mu.Unlock() |
| 301 | if sessInfo == nil && !h.opts.Stateless { |
| 302 | // Unless we're in 'stateless' mode, which doesn't perform any Session-ID |
| 303 | // validation, we require that the session ID matches a known session. |
| 304 | // |
| 305 | // In stateless mode, a temporary transport is be created below. |
| 306 | http.Error(w, "session not found", http.StatusNotFound) |
| 307 | return |
| 308 | } |
| 309 | // Prevent session hijacking: if the session was created with a user ID, |
| 310 | // verify that subsequent requests come from the same user. |
| 311 | if sessInfo != nil && sessInfo.userID != "" { |
| 312 | tokenInfo := auth.TokenInfoFromContext(req.Context()) |