Root cause
SSRF & network boundaries introduced
The initial server implementation read an address from the X-Consul-Address request header and stored it in the request context for the client builder to apply to the Consul endpoint.
How AI contributed: Direct introductionPublished Jul 29, 2026 in
hashicorp/consul-mcp-serverAll 2 findings in this repository
The server bridges MCP clients to a Consul cluster and is deployed with one Consul endpoint and one Consul token. The request path let the client choose that endpoint: a header put an address into the request context, and the client builder replaced the configured address with it, so the server's own credentials stayed attached to the redirected call. Anyone able to reach the server could send its Consul API traffic to a host they controlled and observe the Consul token travelling with it. The hardening change removes the header read and the context override and keeps the Consul address on the server's CONSUL_HTTP_ADDR setting, which closes the redirect.
The request middleware copied an X-Consul-Address header into the request context, and the client builder used that value as the Consul address for every API call made while handling the request. A client could therefore redirect the server's Consul traffic to an endpoint it controlled and read the configured Consul token from the outgoing request the server sent there.
Root cause
The initial server implementation read an address from the X-Consul-Address request header and stored it in the request context for the client builder to apply to the Consul endpoint.
Fix
The client hardening change deletes the header read and the context override, so the Consul endpoint comes only from the server's own CONSUL_HTTP_ADDR configuration.
Fix by Sanika Chavan · no AI marker found
Case facts
@@ -0,0 +141,6 @@+ // Extract Consul address from header+ consulAddr := r.Header.Get(textproto.CanonicalMIMEHeaderKey("X-Consul-Address"))+ if consulAddr != "" {+ ctx = context.WithValue(ctx, "consul_address", consulAddr)+ logger.Debugf("consul_address: %s", consulAddr)+ }@@ -0,0 +65,6 @@+ // override the address and token from session context if available+ if addr, ok := ctx.Value("consul_address").(string); ok && addr != "" {+ address = addr+ }++ if tkn, ok := ctx.Value("consul_token").(string); ok && tkn != "" {@@ -141,5 +138,1 @@ func NewSecurityHandler(handler http.Handler, allowedOrigins []string, corsMode- // Extract Consul address from header- consulAddr := r.Header.Get(textproto.CanonicalMIMEHeaderKey("X-Consul-Address"))- if consulAddr != "" {- ctx = context.WithValue(ctx, "consul_address", consulAddr)- logger.Debugf("consul_address: %s", consulAddr)+ sessionID := r.Header.Get(server.HeaderKeySessionID)@@ -67,6 +72,2 @@ func NewConsulClient(ctx context.Context, sessionId string, logger *log.Logger)- // override the address and token from session context if available- if addr, ok := ctx.Value("consul_address").(string); ok && addr != "" {- address = addr- }-- if tkn, ok := ctx.Value("consul_token").(string); ok && tkn != "" {+ // Override the token from request context if available. The Consul address+ // is intentionally only read from CONSUL_HTTP_ADDR.