VYPR
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.

PackageAffected versionsPatched versions
github.com/safedep/vetGo
< 1.12.51.12.5

Affected products

1
  • Range: v0.0.2-dev, v0.0.4-dev, v0.0.5-dev, …

Patches

1
0ae3560ba118

fix: mcp SSE hardening (#587)

https://github.com/safedep/vetArunanshu BiswasSep 4, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.