Low severityOSV Advisory· Published Sep 29, 2025· Updated Apr 15, 2026
CVE-2025-59163
CVE-2025-59163
Description
vet is an open source software supply chain security tool. Versions 1.12.4 and below are vulnerable to a DNS rebinding attack due to lack of HTTP Host and Origin header validation. Data from the vet scan sqlite3 database may be exposed to remote attackers when vet is used as an MCP server in SSE mode with default ports through the sqlite3 query MCP tool. This issue is fixed in version 1.12.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/safedep/vetGo | < 1.12.5 | 1.12.5 |
Affected products
1Patches
10ae3560ba118fix: mcp SSE hardening (#587)
10 files changed · +631 −49
cmd/server/mcp.go+32 −1 modified@@ -22,6 +22,8 @@ var ( registerVetSQLQueryTool bool vetSQLQueryToolDBPath string registerPackageRegistryTool bool + sseServerAllowedOrigins []string + sseServerAllowedHosts []string ) func newMcpServerCommand() *cobra.Command { @@ -42,6 +44,19 @@ func newMcpServerCommand() *cobra.Command { cmd.Flags().StringVar(&mcpServerSseServerAddr, "sse-server-addr", "localhost:9988", "The address to listen for SSE connections") cmd.Flags().StringVar(&mcpServerServerType, "server-type", "stdio", "The type of server to start (stdio, sse)") + cmd.Flags().StringSliceVar( + &sseServerAllowedOrigins, + "sse-allowed-origins", + nil, + "List of allowed origin prefixes for SSE connections. By default, we allow http://localhost:, http://127.0.0.1: and https://localhost:.", + ) + cmd.Flags().StringSliceVar( + &sseServerAllowedHosts, + "sse-allowed-hosts", + nil, + "List of allowed hosts for SSE connections. By default, we allow localhost:9988, 127.0.0.1:9988 and [::1]:9988.", + ) + // We allow skipping default tools to allow for custom tools to be registered when the server starts. // This is useful for agents to avoid unnecessary tool registration. cmd.Flags().BoolVar(&skipDefaultTools, "skip-default-tools", false, "Skip registering default tools") @@ -75,7 +90,23 @@ func startMcpServer() error { case "stdio": mcpSrv, err = server.NewMcpServerWithStdioTransport(server.DefaultMcpServerConfig()) case "sse": - mcpSrv, err = server.NewMcpServerWithSseTransport(server.DefaultMcpServerConfig()) + config := server.DefaultMcpServerConfig() + + // Override with user supplied config + config.SseServerAddr = mcpServerSseServerAddr + + // override origins and hosts defaults only if user explicitly set them. + // When explicitly passed as cmd line args, cobra parses + // --sse-allowed-hosts='' as empty slice. Otherwise if not provided, + // sse-allowed-hosts will be nil. + if sseServerAllowedOrigins != nil { + config.SseServerAllowedOriginsPrefix = sseServerAllowedOrigins + } + if sseServerAllowedHosts != nil { + config.SseServerAllowedHosts = sseServerAllowedHosts + } + + mcpSrv, err = server.NewMcpServerWithSseTransport(config) default: return fmt.Errorf("invalid server type: %s", mcpServerServerType) }
docs/mcp.md+56 −2 modified@@ -42,6 +42,61 @@ The SSE (Server-Sent Events) transport supports: The SSE endpoint returns appropriate headers for HEAD requests without a body, allowing tools to verify endpoint availability and capabilities. +### Security: Host and Origin Guards + +For SSE, the server enforces simple, user-configurable guards to reduce the risk +of unauthorized cross-origin access and DNS rebinding attacks. + +- **Host guard**: Only allows connections whose `Host` header matches an allowed + host list. +- **Origin guard**: For browser requests, only allows requests whose `Origin` + starts with an allowed prefix. + +These checks are on by default with sensible localhost defaults, and you can +customize them with flags when starting the server. + +#### Defaults + +- **Allowed hosts**: `localhost:9988`, `127.0.0.1:9988`, `[::1]:9988` +- **Allowed origin prefixes**: `http://localhost:`, `http://127.0.0.1:`, `https://localhost:` + +Requests that fail the host check are rejected with status `403`, and requests +that fail the origin check are rejected with status `403`. + +#### Customize allowed hosts and origins + +You can override the defaults using the following flags: + +```bash +vet server mcp \ + --server-type sse \ + --sse-allowed-hosts "localhost:8080,127.0.0.1:8080" \ + --sse-allowed-origins "http://localhost:,https://localhost:" +``` + +If you are running behind a proxy or using a different port, set both lists to +match your environment. For example, when exposing SSE on port 3001: + +```bash +vet server mcp \ + --server-type sse \ + --sse-allowed-hosts "localhost:3001,127.0.0.1:3001" \ + --sse-allowed-origins "http://localhost:,http://127.0.0.1:,https://localhost:" +``` + +With Docker, append the same flags to the container command: + +```bash +docker run --rm -i ghcr.io/safedep/vet:latest \ + server mcp \ + --server-type sse \ + --sse-allowed-hosts "localhost:9988,127.0.0.1:9988" \ + --sse-allowed-origins "http://localhost:,http://127.0.0.1:,https://localhost:" +``` + +Tip: Non-browser clients may omit the `Origin` header. Those requests are +allowed as long as the host guard passes. + ## Configure MCP Client > **Note:** The example below uses pre-build docker image. You can build your own by running @@ -146,7 +201,7 @@ Add `vet-mcp` server to `.vscode/mcp.json` (project specific configuration) } ``` -In order to use `vet-mcp` for all projects in Visual Studio Code, add following `mcp` setting in [Visual Studio Code User Settings](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-user-settings) (`settings.json`) +In order to use `vet-mcp` for all projects in Visual Studio Code, add following `mcp` setting in [Visual Studio Code User Settings](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-user-settings) (`settings.json`) ```json { @@ -170,7 +225,6 @@ In order to use `vet-mcp` for all projects in Visual Studio Code, add following } ``` - Add the following to `.github/copilot-instructions.md` file: ```
go.mod+0 −1 modified@@ -69,7 +69,6 @@ require ( 4d63.com/gochecknoglobals v0.2.2 // indirect ariga.io/atlas v0.34.0 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20240508200655-46a4cf4ba109.1 // indirect - buf.build/gen/go/safedep/api/connectrpc/go v1.18.1-20250822112533-a008e1948f1d.1 // indirect cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.121.2 // indirect cloud.google.com/go/auth v0.16.1 // indirect
go.sum+0 −12 modified@@ -4,20 +4,10 @@ 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= ariga.io/atlas v0.34.0 h1:4hdy+2x+xNs6Lx2anuJ/4Q7lCaqddbEj5CtRDVOBu0M= ariga.io/atlas v0.34.0/go.mod h1:WJesu2UCpGQvgUh3oVP94EiRT61nNy1W/VN5g+vqP1I= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20240508200655-46a4cf4ba109.1 h1:7JbSS7TE2PJR4d/qRtynipwLl/CBFoTB69pX7xlhcJM= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20240508200655-46a4cf4ba109.1/go.mod h1:8EQ5GzyGJQ5tEIwMSxCl8RKJYsjCpAwkdcENoioXT6g= -buf.build/gen/go/safedep/api/connectrpc/go v1.18.1-20250822112533-a008e1948f1d.1 h1:l2Fuy7PMz0wR8sQVQlhMnm6fxr6ZLdWeAR7NzZ5w2jI= -buf.build/gen/go/safedep/api/connectrpc/go v1.18.1-20250822112533-a008e1948f1d.1/go.mod h1:W2eqH9M5zldL2cDL9xqE/HsWe2FoWw+Bwzaul95RQko= -buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250610075857-7cfdb61a0bfa.2 h1:ENbt9SmU2gh4YhjcFqzceJRlg80hsD28M+Oon9l752A= -buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250610075857-7cfdb61a0bfa.2/go.mod h1:WDOWZglnweQ4njVEJpLYYpLMx9fD+e94KbKdt8oJrxY= buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250819072717-b69aa2c62a0d.2 h1:A4enKVmVf69uVSG88POR59z5YE6dhATNLpL8+DmZtsg= buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250819072717-b69aa2c62a0d.2/go.mod h1:Raps9oq+lWS0tdif5yUy8MS6UGc2pr6NMSrv3Jz4avM= -buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1 h1:4sM5O5dx0yUucJ1trjZ8Cm9IGX2loEc4cUyh3Xy+5eU= -buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I= -buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.8-20250819072717-b69aa2c62a0d.1 h1:fRdyfm5aiolcZmJuWPzbbI4cSYJlssvBZXi/BQUfMWc= -buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.8-20250819072717-b69aa2c62a0d.1/go.mod h1:Q5oZou54kSUyZHl4RSPY93qr3b1ssj3ZvdBAhRAdlJA= buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.8-20250822112533-a008e1948f1d.1 h1:XqV9omaTxxXaI9VvS87PX4Uw6h927UycRR7SfwENSHU= buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.8-20250822112533-a008e1948f1d.1/go.mod h1:Q5oZou54kSUyZHl4RSPY93qr3b1ssj3ZvdBAhRAdlJA= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= @@ -2066,8 +2056,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
mcp/server/guard.go+56 −0 added@@ -0,0 +1,56 @@ +package server + +import ( + "net/http" + "slices" + "strings" +) + +// hostGuard is a middleware that allows only the allowed hosts to access the +// MCP server. nil config.SseServerAllowedHosts will use the default allowed hosts. Empty +// config.SseServerAllowedHosts will block all hosts. +func hostGuard(config McpServerConfig, next http.Handler) http.Handler { + allowedHosts := config.SseServerAllowedHosts + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // contains is faster than a map lookup for small lists + if !slices.Contains(allowedHosts, r.Host) { + w.WriteHeader(http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + +// originGuard is a middleware that allows only the allowed origins to access +// the MCP server. If allowedOriginsPrefix is nil or empty, all origins will be blocked. +func originGuard(config McpServerConfig, next http.Handler) http.Handler { + allowedOriginsPrefix := config.SseServerAllowedOriginsPrefix + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + o := r.Header.Get("Origin") + if o == "" { + // Non-browser/same-origin fetches may omit Origin. Don't block + // solely on this. + next.ServeHTTP(w, r) + return + } + + if !isAllowedOrigin(o, allowedOriginsPrefix) { + http.Error(w, "forbidden origin", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// isAllowedOrigin checks if the origin is in the allowed origins prefix list. +func isAllowedOrigin(origin string, allowedOriginsPrefix []string) bool { + for _, allowedOriginPrefix := range allowedOriginsPrefix { + if strings.HasPrefix(origin, allowedOriginPrefix) { + return true + } + } + return false +}
mcp/server/guard_test.go+293 −0 added@@ -0,0 +1,293 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHostGuard(t *testing.T) { + // Create a mock handler that just returns OK + mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Wrap with host guard (using default hardcoded hosts for backwards compatibility) + config := McpServerConfig{ + SseServerAllowedHosts: []string{"localhost:9988", "127.0.0.1:9988", "[::1]:9988"}, + } + hostGuardedHandler := hostGuard(config, mockHandler) + + tests := []struct { + name string + host string + expectedStatus int + shouldAllow bool + }{ + { + name: "localhost:9988 should be allowed", + host: "localhost:9988", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "127.0.0.1:9988 should be allowed", + host: "127.0.0.1:9988", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "[::1]:9988 should be allowed", + host: "[::1]:9988", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "different host should be blocked", + host: "example.com:9988", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "localhost with different port should be blocked", + host: "localhost:9999", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "127.0.0.1 with different port should be blocked", + host: "127.0.0.1:9999", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "IPv6 localhost with different port should be blocked", + host: "[::1]:9999", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "malicious host should be blocked", + host: "evil.com:9988", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "host without port should be blocked", + host: "localhost", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Host = tt.host + w := httptest.NewRecorder() + + hostGuardedHandler.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + if tt.shouldAllow { + assert.Equal( + t, + http.StatusOK, + w.Code, + "Request should be allowed for host: %s", + tt.host, + ) + } else { + assert.Equal(t, http.StatusForbidden, w.Code, "Request should be blocked for host: %s", tt.host) + } + }) + } +} + +func TestOriginGuard(t *testing.T) { + // Create a mock handler that just returns OK + mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Wrap with origin guard (using empty list to fall back to default localhost logic) + originGuardedHandler := originGuard(McpServerConfig{ + SseServerAllowedOriginsPrefix: []string{ + "http://localhost:", + "http://127.0.0.1:", + "https://localhost:", + }, + }, mockHandler) + + tests := []struct { + name string + origin string + expectedStatus int + shouldAllow bool + }{ + { + name: "http://localhost:3000 should be allowed", + origin: "http://localhost:3000", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "http://127.0.0.1:3000 should be allowed", + origin: "http://127.0.0.1:3000", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "https://localhost:3000 should be allowed", + origin: "https://localhost:3000", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "http://localhost:8080 should be allowed", + origin: "http://localhost:8080", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "no origin header should be allowed (non-browser requests)", + origin: "", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "http://example.com should be blocked", + origin: "http://example.com", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "https://example.com should be blocked", + origin: "https://example.com", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "http://evil.com:3000 should be blocked", + origin: "http://evil.com:3000", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "ftp://localhost:3000 should be blocked", + origin: "ftp://localhost:3000", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "null origin should be blocked", + origin: "null", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + if tt.origin != "" { + req.Header.Set("Origin", tt.origin) + } + w := httptest.NewRecorder() + + originGuardedHandler.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + if tt.shouldAllow { + assert.Equal( + t, + http.StatusOK, + w.Code, + "Request should be allowed for origin: %s", + tt.origin, + ) + } else { + assert.Equal(t, http.StatusForbidden, w.Code, "Request should be blocked for origin: %s", tt.origin) + } + }) + } +} + +func TestGuardsIntegration(t *testing.T) { + // Create a mock handler that just returns OK + mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Wrap with both guards (in the same order as used in sse.go) + config := McpServerConfig{ + SseServerAllowedOriginsPrefix: []string{"http://localhost:"}, + SseServerAllowedHosts: []string{"localhost:9988", "127.0.0.1:9988", "[::1]:9988"}, + } + wrappedHandler := originGuard(config, mockHandler) + wrappedHandler = hostGuard(config, wrappedHandler) + + tests := []struct { + name string + host string + origin string + expectedStatus int + shouldAllow bool + }{ + { + name: "valid host and origin should be allowed", + host: "localhost:9988", + origin: "http://localhost:3000", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + { + name: "invalid host should be blocked regardless of origin", + host: "example.com:9988", + origin: "http://localhost:3000", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "valid host but invalid origin should be blocked", + host: "localhost:9988", + origin: "http://example.com", + expectedStatus: http.StatusForbidden, + shouldAllow: false, + }, + { + name: "no origin header with valid host should be allowed", + host: "127.0.0.1:9988", + origin: "", + expectedStatus: http.StatusOK, + shouldAllow: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Host = tt.host + if tt.origin != "" { + req.Header.Set("Origin", tt.origin) + } + w := httptest.NewRecorder() + + wrappedHandler.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + if tt.shouldAllow { + assert.Equal( + t, + http.StatusOK, + w.Code, + "Request should be allowed for host=%s, origin=%s", + tt.host, + tt.origin, + ) + } + }) + } +}
mcp/server/server.go+16 −0 modified@@ -12,6 +12,10 @@ type McpServerConfig struct { SseServerBasePath string SseServerAddr string + + // For security reasons, restrict allowed origins and hosts for SSE connections + SseServerAllowedOriginsPrefix []string + SseServerAllowedHosts []string } func DefaultMcpServerConfig() McpServerConfig { @@ -24,6 +28,18 @@ func DefaultMcpServerConfig() McpServerConfig { // SSE server will automatically add `/sse` to the base path SseServerBasePath: "", SseServerAddr: "localhost:9988", + + // By default, we use the current hardcoded values for backwards compatibility + // Users can customize these lists as needed for their deployment environment + SseServerAllowedHosts: []string{"localhost:9988", "127.0.0.1:9988", "[::1]:9988"}, + + // We allow common localhost origins by default for better usability + // Users should explicitly set allowed origins based on their deployment environment + SseServerAllowedOriginsPrefix: []string{ + "http://localhost:", + "http://127.0.0.1:", + "https://localhost:", + }, } }
mcp/server/sse.go+18 −4 modified@@ -14,15 +14,23 @@ func sseHandlerWithHeadSupport(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Only handle HEAD requests to the SSE endpoint specifically if r.Method == http.MethodHead && r.URL.Path == "/sse" { - // For HEAD requests to SSE endpoint, set the same headers as SSE connections but don't send a body + // For HEAD requests to SSE endpoint, set the same headers as SSE + // connections but don't send a body w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") + + // Set CORS headers based on the request origin (will be validated + // by originGuard middleware) + if origin := r.Header.Get("Origin"); origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + } + w.WriteHeader(http.StatusOK) return } - // For all other requests (including GET, and HEAD to other endpoints), delegate to the original handler + // For all other requests (including GET, and HEAD to other endpoints), + // delegate to the original handler handler.ServeHTTP(w, r) }) } @@ -34,10 +42,16 @@ func NewMcpServerWithSseTransport(config McpServerConfig) (*mcpServer, error) { server: srv, servingFunc: func(srv *mcpServer) error { logger.Infof("Starting MCP server with SSE transport: %s", config.SseServerAddr) - s := server.NewSSEServer(srv.server, server.WithStaticBasePath(config.SseServerBasePath)) + s := server.NewSSEServer( + srv.server, + server.WithStaticBasePath(config.SseServerBasePath), + ) // Wrap the SSE server with HEAD request support wrappedHandler := sseHandlerWithHeadSupport(s) + wrappedHandler = originGuard(config, wrappedHandler) + wrappedHandler = hostGuard(config, wrappedHandler) + httpServer := &http.Server{ Addr: config.SseServerAddr, Handler: wrappedHandler,
mcp/server/sse_integration_test.go+120 −11 modified@@ -2,6 +2,7 @@ package server import ( "context" + "net" "net/http" "net/http/httptest" "testing" @@ -17,13 +18,30 @@ func TestSSEServerIntegration(t *testing.T) { mcpServer := server.NewMCPServer("test-vet-mcp", "0.0.1", server.WithInstructions("Test MCP server for integration testing")) - // Create SSE server with our custom handler + // Create SSE server with our custom handler and wrap with guards sseServer := server.NewSSEServer(mcpServer, server.WithStaticBasePath("")) - wrappedHandler := sseHandlerWithHeadSupport(sseServer) - - // Create test server - testServer := httptest.NewServer(wrappedHandler) - defer testServer.Close() + baseHandler := sseHandlerWithHeadSupport(sseServer) + + // Use an unstarted server with a custom listener so we know the allowed host + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + allowedHost := listener.Addr().String() + + // Apply guards in the same order as production code: origin, then host + config := McpServerConfig{ + SseServerAllowedHosts: []string{allowedHost}, + SseServerAllowedOriginsPrefix: []string{allowedHost}, + } + wrappedHandler := originGuard(config, baseHandler) + wrappedHandler = hostGuard(config, wrappedHandler) + + // Create and start test server + testServer := httptest.NewUnstartedServer(wrappedHandler) + testServer.Listener = listener + testServer.Start() + t.Cleanup(func() { + testServer.Close() + }) t.Run("HEAD request to SSE endpoint", func(t *testing.T) { req, err := http.NewRequest(http.MethodHead, testServer.URL+"/sse", nil) @@ -32,7 +50,9 @@ func TestSSEServerIntegration(t *testing.T) { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) require.NoError(t, err) - defer resp.Body.Close() + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) // Check status code assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -41,12 +61,44 @@ func TestSSEServerIntegration(t *testing.T) { assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) assert.Equal(t, "no-cache", resp.Header.Get("Cache-Control")) assert.Equal(t, "keep-alive", resp.Header.Get("Connection")) - assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin")) + // No Origin header was sent, so no CORS header should be set for non-browser requests + assert.Equal(t, "", resp.Header.Get("Access-Control-Allow-Origin")) // Verify no body was returned for HEAD request (ContentLength -1 is expected for HEAD) assert.True(t, resp.ContentLength <= 0, "HEAD request should not have content length > 0") }) + t.Run("GET request with invalid host should be blocked", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/sse", nil) + require.NoError(t, err) + // Override host header to simulate a different host + req.Host = "example.com:9988" + + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("GET request with invalid origin should be blocked", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/sse", nil) + require.NoError(t, err) + req.Header.Set("Origin", "http://example.com") + + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + t.Run("GET request to SSE endpoint", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, testServer.URL+"/sse", nil) require.NoError(t, err) @@ -59,7 +111,9 @@ func TestSSEServerIntegration(t *testing.T) { client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Do(req) require.NoError(t, err) - defer resp.Body.Close() + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) // Check status code assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -78,7 +132,9 @@ func TestSSEServerIntegration(t *testing.T) { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) require.NoError(t, err) - defer resp.Body.Close() + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) // POST to SSE endpoint should return 405 Method Not Allowed since SSE only accepts GET/HEAD assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) @@ -91,10 +147,63 @@ func TestSSEServerIntegration(t *testing.T) { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) require.NoError(t, err) - defer resp.Body.Close() + t.Cleanup(func() { + assert.NoError(t, resp.Body.Close()) + }) // HEAD requests to message endpoint should be handled by original SSE server handler // which returns 400 Bad Request because message handler expects POST with sessionId parameter assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) } + +func TestSSEServer_AllowsCustomConfiguredOrigin(t *testing.T) { + // Create a test MCP server + mcpServer := server.NewMCPServer("test-vet-mcp", "0.0.1", + server.WithInstructions("Test MCP server for allowed origin")) + + // Create SSE server and base handler + sseServer := server.NewSSEServer(mcpServer, server.WithStaticBasePath("")) + baseHandler := sseHandlerWithHeadSupport(sseServer) + + // Bind to a random local port and capture host:port for allowed list + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + allowedHost := listener.Addr().String() + + // Configure guards to allow origins that start with a custom domain (non-default) + customOriginPrefix := "http://custom-origin.test:" + config := McpServerConfig{ + SseServerAllowedHosts: []string{allowedHost}, + SseServerAllowedOriginsPrefix: []string{customOriginPrefix}, + } + + // Apply origin then host guard as in production + wrapped := originGuard(config, baseHandler) + wrapped = hostGuard(config, wrapped) + + // Start test server with the custom listener + testServer := httptest.NewUnstartedServer(wrapped) + testServer.Listener = listener + testServer.Start() + t.Cleanup(func() { testServer.Close() }) + + t.Run("GET to /sse with allowed custom origin should succeed", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/sse", nil) + require.NoError(t, err) + req.Header.Set("Origin", customOriginPrefix+"3000") + + // Use timeout to avoid hanging on SSE + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + req = req.WithContext(ctx) + + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, resp.Body.Close()) }) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) + }) +}
mcp/server/sse_test.go+40 −18 modified@@ -27,60 +27,82 @@ func TestSSEHandlerWithHeadSupport(t *testing.T) { wrappedHandler := sseHandlerWithHeadSupport(mockSSEHandler) tests := []struct { - name string - method string - path string - expectedStatus int + name string + method string + path string + origin string + expectedStatus int expectedHeaders map[string]string - expectBody bool + expectBody bool }{ { name: "HEAD request to SSE endpoint should return SSE headers without body", method: http.MethodHead, path: "/sse", + origin: "", // non-browser request + expectedStatus: http.StatusOK, + expectedHeaders: map[string]string{ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + // No CORS header expected for non-browser requests + }, + expectBody: false, + }, + { + name: "HEAD request to SSE endpoint with origin should return SSE headers with CORS", + method: http.MethodHead, + path: "/sse", + origin: "http://localhost:3000", expectedStatus: http.StatusOK, expectedHeaders: map[string]string{ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Origin": "http://localhost:3000", }, expectBody: false, }, { name: "GET request to SSE endpoint should work normally", method: http.MethodGet, path: "/sse", + origin: "", // No origin header - handled by mock handler expectedStatus: http.StatusOK, expectedHeaders: map[string]string{ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Origin": "*", // Set by mock handler for GET }, expectBody: true, }, { - name: "POST request to SSE endpoint should be rejected", - method: http.MethodPost, - path: "/sse", - expectedStatus: http.StatusMethodNotAllowed, + name: "POST request to SSE endpoint should be rejected", + method: http.MethodPost, + path: "/sse", + origin: "", + expectedStatus: http.StatusMethodNotAllowed, expectedHeaders: map[string]string{}, - expectBody: true, // Error message body + expectBody: true, // Error message body }, { - name: "HEAD request to non-SSE endpoint should be passed through", - method: http.MethodHead, - path: "/message", - expectedStatus: http.StatusMethodNotAllowed, + name: "HEAD request to non-SSE endpoint should be passed through", + method: http.MethodHead, + path: "/message", + origin: "", + expectedStatus: http.StatusMethodNotAllowed, expectedHeaders: map[string]string{}, - expectBody: true, // Error message body + expectBody: true, // Error message body }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.path, nil) + if tt.origin != "" { + req.Header.Set("Origin", tt.origin) + } w := httptest.NewRecorder() wrappedHandler.ServeHTTP(w, req) @@ -115,4 +137,4 @@ func TestMcpServerWithSseTransport(t *testing.T) { assert.Equal(t, config, srv.config) assert.NotNil(t, srv.server) assert.NotNil(t, srv.servingFunc) -} \ No newline at end of file +}
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
6- github.com/advisories/GHSA-6q9c-m9fr-865mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59163ghsaADVISORY
- github.com/safedep/vet/commit/0ae3560ba11846375812377299fe078d45cc3d48nvdWEB
- github.com/safedep/vet/releases/tag/v1.12.5nvdWEB
- github.com/safedep/vet/security/advisories/GHSA-6q9c-m9fr-865mnvdWEB
- pkg.go.dev/vuln/GO-2025-3986ghsaWEB
News mentions
0No linked articles in our index yet.