CVE-2026-9739
Description
Vulnerable to DNS rebinding attacks when using SSE (http://b/499408790). During the beta phase, we implemented allowed-origins and allowed-hosts flags to align with MCP security guidelines. However, the hardcoded Access-Control-Allow-Origin: * header in the SSE initialization handler was inadvertently retained. This vulnerability specifically impacts users connecting via Toolbox using SSE under specification v2024-11-05.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Hardcoded CORS bypass in MCP Toolbox SSE handler allows DNS rebinding and cross-origin attacks, enabling session hijacking and tool execution.
Vulnerability
The MCP Toolbox (formerly genai-toolbox) contains a vulnerability in its Server-Sent Events (SSE) handler within internal/server/mcp.go:370, where a hardcoded Access-Control-Allow-Origin: * header is set. This overrides the global CORS middleware configured via the --allowed-origins and --allowed-hosts flags, making the policy ineffective for the SSE endpoint. The vulnerability affects users connecting via Toolbox using SSE under specification v2024-11-05 [1].
Exploitation
An attacker can exploit this by luring a victim to a malicious website that establishes a cross-origin SSE connection to the Toolbox. The hardcoded header allows any origin to connect, enabling the attacker to hijack session IDs and execute arbitrary tools on behalf of the victim without proper authorization [1].
Impact
Successful exploitation results in a security policy bypass, cross-site request forgery (CSRF), and session hijacking. The attacker can use the Toolbox as a proxy to exfiltrate data from databases (e.g., Postgres, BigQuery) configured in the Toolbox, leading to significant data disclosure and potential compromise of backend systems [1].
Mitigation
The fix was implemented in pull request #3054, which removes the hardcoded Access-Control-Allow-Origin: * header from internal/server/mcp.go:370. The global CORS middleware in internal/server/server.go should be used instead. The fix was merged on May 7, 2026 [2]. Users are advised to update their MCP Toolbox to the latest version that includes this patch.
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)
Patches
1c4c7bd917e68fix: remove hardcoded * allowed origin for sse (#3054)
4 files changed · +310 −14
internal/server/mcp.go+0 −1 modified@@ -367,7 +367,6 @@ func sseHandler(s *Server, w http.ResponseWriter, r *http.Request) { 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", "*") // Define attributes for session metrics networkProtocolVersion := fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor)
internal/server/mcp_test.go+0 −4 modified@@ -990,7 +990,6 @@ func TestSseEndpoint(t *testing.T) { contentType := "text/event-stream" cacheControl := "no-cache" connection := "keep-alive" - accessControlAllowOrigin := "*" testCases := []struct { name string @@ -1056,9 +1055,6 @@ func TestSseEndpoint(t *testing.T) { if gotConnection := resp.Header.Get("Connection"); gotConnection != connection { t.Fatalf("unexpected content-type header: want %s, got %s", connection, gotConnection) } - if gotAccessControlAllowOrigin := resp.Header.Get("Access-Control-Allow-Origin"); gotAccessControlAllowOrigin != accessControlAllowOrigin { - t.Fatalf("unexpected cache-control header: want %s, got %s", accessControlAllowOrigin, gotAccessControlAllowOrigin) - } buffer := make([]byte, 1024) n, err := resp.Body.Read(buffer)
internal/server/server.go+4 −0 modified@@ -569,3 +569,7 @@ func (s *Server) Shutdown(ctx context.Context) error { s.logger.DebugContext(ctx, "shutting down the server.") return s.srv.Shutdown(ctx) } + +func (s *Server) Addr() string { + return s.listener.Addr().String() +}
internal/server/server_test.go+306 −9 modified@@ -19,10 +19,12 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/http/httptest" "os" "reflect" + "slices" "strings" "testing" @@ -88,13 +90,9 @@ func TestServe(t *testing.T) { } // start server in background - errCh := make(chan error) go func() { - defer close(errCh) - - err = s.Serve(ctx) - if err != nil { - errCh <- err + if err := s.Serve(ctx); err != nil && err != http.ErrServerClosed { + t.Errorf("server serve error: %v", err) } }() @@ -205,6 +203,307 @@ func TestUpdateServer(t *testing.T) { } } +func TestEndpointSecurityAllowedOrigin(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("error setting up logger: %s", err) + } + + testCases := []struct { + desc string + allowedOrigins []string + origin string + corsBlocked bool + }{ + { + desc: "allowed origin all", + allowedOrigins: []string{"*"}, + origin: "https://evil.com", + }, + { + desc: "allowed origin trusted with trusted origin", + allowedOrigins: []string{"https://trusted.com"}, + origin: "https://trusted.com", + }, + { + desc: "allowed origin trusted with evil origin", + allowedOrigins: []string{"https://trusted.com"}, + origin: "https://evil.com", + corsBlocked: true, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + addr, port := "127.0.0.1", 0 + cfg := server.ServerConfig{ + Version: "0.0.0", + Address: addr, + Port: port, + EnableAPI: true, + AllowedOrigins: tc.allowedOrigins, + AllowedHosts: []string{"*"}, + } + + instrumentation, err := telemetry.CreateTelemetryInstrumentation(cfg.Version) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + ctx = util.WithInstrumentation(ctx, instrumentation) + + s, err := server.NewServer(ctx, cfg) + if err != nil { + t.Fatalf("error setting up server: %s", err) + } + + err = s.Listen(ctx) + if err != nil { + t.Fatalf("unable to start server: %v", err) + } + + urlAddr := s.Addr() + + // start server in background + go func() { + if err := s.Serve(ctx); err != nil && err != http.ErrServerClosed { + t.Errorf("server serve error: %v", err) + } + }() + + // test every endpoints that we support in Toolbox + endpoints := []struct { + desc string + requestType string + url string + }{ + { + desc: "GET api toolset", + requestType: "GET", + url: "/api/toolset", + }, + { + desc: "GET api tool", + requestType: "GET", + url: "/api/tool/tool_one", + }, + { + desc: "POST api tool", + requestType: "POST", + url: "/api/tool/tool_one/invoke", + }, + { + desc: "GET mcp sse", + requestType: "GET", + url: "/mcp/sse", + }, + { + desc: "GET mcp", + requestType: "GET", + url: "/mcp", + }, + { + desc: "POST mcp", + requestType: "POST", + url: "/mcp", + }, + { + desc: "DELETE mcp", + requestType: "DELETE", + url: "/mcp", + }, + } + for _, e := range endpoints { + t.Run(e.desc, func(t *testing.T) { + url := fmt.Sprintf("http://%s%s", urlAddr, e.url) + client := &http.Client{} + req, err := http.NewRequest(e.requestType, url, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Origin", tc.origin) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + gotOrigin := resp.Header.Get("Access-Control-Allow-Origin") + if !tc.corsBlocked { + // if cors is not blocked, the origin header should be + // within allowedOrigins + if !slices.Contains(tc.allowedOrigins, gotOrigin) { + t.Errorf(`origin "%s" is not part of allowed origins %s`, gotOrigin, tc.allowedOrigins) + } + } else if tc.corsBlocked { + // if cors is blocked, the origin header should not + // contain origin + if gotOrigin == "*" { + t.Errorf("REGRESSION: Server is forcing a wildcard '*' header!") + } + if gotOrigin == tc.origin { + t.Errorf("server allowed an origin not in the whitelist: %s", gotOrigin) + } + } + }) + } + }) + } +} + +func TestEndpointSecurityAllowedHost(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("error setting up logger: %s", err) + } + + testCases := []struct { + desc string + allowedHosts []string + host string + wantStatus int + }{ + { + desc: "allowed hosts all", + allowedHosts: []string{"*"}, + host: "evil.com", + wantStatus: http.StatusOK, + }, + { + desc: "allowed hosts trusted with trusted host", + allowedHosts: []string{"trusted.com"}, + host: "trusted.com", + wantStatus: http.StatusOK, + }, + { + desc: "allowed hosts trusted with evil host", + allowedHosts: []string{"trusted.com"}, + host: "evil.com", + wantStatus: http.StatusForbidden, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + addr, port := "127.0.0.1", 0 + cfg := server.ServerConfig{ + Version: "0.0.0", + Address: addr, + Port: port, + EnableAPI: true, + AllowedHosts: tc.allowedHosts, + } + + instrumentation, err := telemetry.CreateTelemetryInstrumentation(cfg.Version) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + ctx = util.WithInstrumentation(ctx, instrumentation) + + s, err := server.NewServer(ctx, cfg) + if err != nil { + t.Fatalf("error setting up server: %s", err) + } + + err = s.Listen(ctx) + if err != nil { + t.Fatalf("unable to start server: %v", err) + } + + urlAddr := s.Addr() + _, actualPort, err := net.SplitHostPort(urlAddr) + if err != nil { + t.Fatalf("failed to parse server address: %v", err) + } + hostWithPort := net.JoinHostPort(tc.host, actualPort) + + // start server in background + go func() { + if err := s.Serve(ctx); err != nil && err != http.ErrServerClosed { + t.Errorf("server serve error: %v", err) + } + }() + + // test every endpoints that we support in Toolbox + endpoints := []struct { + desc string + requestType string + url string + requestErr int + errStr string + }{ + { + desc: "GET api toolset", + requestType: "GET", + url: "/api/toolset", + }, + { + desc: "GET api tool", + requestType: "GET", + url: "/api/tool/tool_one", + requestErr: http.StatusNotFound, + errStr: "invalid tool name", + }, + { + desc: "POST api tool", + requestType: "POST", + url: "/api/tool/tool_one/invoke", + requestErr: http.StatusNotFound, + errStr: "invalid tool name", + }, + { + desc: "GET mcp sse", + requestType: "GET", + url: "/mcp/sse", + }, + { + desc: "GET mcp", + requestType: "GET", + url: "/mcp", + requestErr: http.StatusMethodNotAllowed, + errStr: "toolbox does not support streaming in streamable HTTP transport", + }, + { + desc: "POST mcp", + requestType: "POST", + url: "/mcp", + }, + { + desc: "DELETE mcp", + requestType: "DELETE", + url: "/mcp", + }, + } + for _, e := range endpoints { + t.Run(e.desc, func(t *testing.T) { + url := fmt.Sprintf("http://%s%s", urlAddr, e.url) + client := &http.Client{} + req, err := http.NewRequest(e.requestType, url, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Host = hostWithPort + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != tc.wantStatus { + bodyBytes, _ := io.ReadAll(resp.Body) + if resp.StatusCode == e.requestErr { + if !strings.Contains(string(bodyBytes), e.errStr) { + t.Fatalf("got err %s, expected error %s", string(bodyBytes), e.errStr) + } + return + } + t.Fatalf("expected status %d, got %d: %s", tc.wantStatus, resp.StatusCode, string(bodyBytes)) + } + }) + } + }) + } +} + func TestNameValidation(t *testing.T) { testCases := []struct { desc string @@ -336,11 +635,9 @@ func TestPRMEndpoint(t *testing.T) { t.Fatalf("unable to start server: %v", err) } - errCh := make(chan error) go func() { - defer close(errCh) if err := s.Serve(ctx); err != nil && err != http.ErrServerClosed { - errCh <- err + t.Errorf("server serve error: %v", err) } }() defer func() {
Vulnerability mechanics
Root cause
"Hardcoded `Access-Control-Allow-Origin: *` header in the SSE handler overrides the global CORS middleware, bypassing the `allowed-origins` security flag."
Attack vector
An attacker hosts a malicious website that makes cross-origin requests to the victim's Toolbox SSE endpoint (`/mcp/sse`). Because the SSE handler hardcodes `Access-Control-Allow-Origin: *` [patch_id=2799071], the browser's Same-Origin Policy is bypassed and the malicious site can read responses. This enables DNS rebinding attacks [ref_id=1] and allows the attacker to hijack session IDs and execute arbitrary tools configured in the toolbox (e.g., Postgres, BigQuery) on behalf of the victim user [ref_id=1]. The vulnerability only affects users connecting via SSE under MCP specification v2024-11-05 [patch_id=2799071].
Affected code
The vulnerable code is in `internal/server/mcp.go` within the `sseHandler` function, where line 370 hardcoded `w.Header().Set("Access-Control-Allow-Origin", "*")` [patch_id=2799071][ref_id=1]. This overrode the global CORS middleware defined in `internal/server/server.go` [ref_id=1].
What the fix does
The patch removes the single line `w.Header().Set("Access-Control-Allow-Origin", "*")` from `internal/server/mcp.go` in the `sseHandler` function [patch_id=2799071]. This deletion allows the global CORS middleware (configured via the `--allowed-origins` flag) to govern cross-origin access for the SSE endpoint, restoring the intended security policy. The commit also adds regression tests in `internal/server/server_test.go` that verify the `Access-Control-Allow-Origin` header respects the configured `allowedOrigins` list and that blocked origins do not receive a wildcard or matching origin header [patch_id=2799071].
Preconditions
- configToolbox must be configured with SSE transport under MCP specification v2024-11-05
- networkAttacker must be able to induce a victim's browser to make cross-origin requests to the Toolbox SSE endpoint
- authVictim must be authenticated to the Toolbox instance at the time of the attack
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.