Consul Vulnerable To Reflected XSS On Content-Type Error Manipulation
Description
A vulnerability was identified in Consul and Consul Enterprise such that the server response did not explicitly set a Content-Type HTTP header, allowing user-provided inputs to be misinterpreted and lead to reflected XSS.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Consul server responses lack a Content-Type header, allowing reflected XSS via user-supplied input.
CVE-2024-10086 describes a reflected cross-site scripting (XSS) vulnerability in HashiCorp Consul and Consul Enterprise. The root cause is that the server's HTTP responses do not explicitly set a Content-Type header, leaving the browser to interpret the response content. If an attacker can inject arbitrary HTML or JavaScript into a response (e.g., via a crafted URL), the browser may render it as HTML, enabling XSS [1].
Exploitation requires an attacker to convince a user to visit a maliciously crafted URL. The attack is network-based with low complexity and does not require authentication or special privileges. The vulnerable endpoints include those that return user-controlled data without a proper Content-Type, such as error pages or debug handlers [2].
A successful attack could allow an attacker to execute arbitrary JavaScript in the context of the victim's session with the Consul UI. This could lead to session hijacking, credential theft, or unauthorized actions on the Consul cluster [1].
The vulnerability was fixed in a commit that explicitly sets the Content-Type header to text/plain; charset=utf-8 for responses that may include user input. Users should upgrade to the latest patched version of Consul to mitigate this issue [2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/hashicorp/consulGo | >= 1.4.1, < 1.20.0 | 1.20.0 |
Affected products
30- osv-coords28 versionspkg:apk/chainguard/consul-1.19pkg:apk/chainguard/consul-1.20pkg:apk/chainguard/consul-1.20-oci-entrypointpkg:apk/chainguard/consul-1.20-oci-entrypoint-compatpkg:apk/chainguard/consul-1.21pkg:apk/chainguard/consul-1.21-oci-entrypointpkg:apk/chainguard/consul-1.21-oci-entrypoint-compatpkg:apk/chainguard/consul-1.22pkg:apk/chainguard/consul-1.22-oci-entrypointpkg:apk/chainguard/consul-1.22-oci-entrypoint-compatpkg:apk/chainguard/consul-fips-1.19pkg:apk/chainguard/consul-fips-1.20pkg:apk/chainguard/consul-fips-1.20-oci-entrypointpkg:apk/chainguard/consul-fips-1.20-oci-entrypoint-compatpkg:apk/chainguard/consul-fips-1.21pkg:apk/chainguard/consul-fips-1.21-oci-entrypointpkg:apk/chainguard/consul-fips-1.21-oci-entrypoint-compatpkg:apk/chainguard/consul-fips-1.22pkg:apk/chainguard/consul-fips-1.22-oci-entrypointpkg:apk/chainguard/consul-fips-1.22-oci-entrypoint-compatpkg:bitnami/consulpkg:golang/github.com/hashicorp/consulpkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Tumbleweedpkg:rpm/suse/govulncheck-vulndb&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP5pkg:rpm/suse/govulncheck-vulndb&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP6pkg:rpm/suse/govulncheck-vulndb&distro=SUSE%20Package%20Hub%2012
< 1.19.2-r47+ 27 more
- (no CPE)range: < 1.19.2-r47
- (no CPE)range: < 1.20.6-r11
- (no CPE)range: < 1.20.6-r11
- (no CPE)range: < 1.20.6-r11
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.22.1-r2
- (no CPE)range: < 1.22.1-r2
- (no CPE)range: < 1.22.1-r2
- (no CPE)range: < 1.19.2-r47
- (no CPE)range: < 1.20.6-r10
- (no CPE)range: < 1.20.6-r10
- (no CPE)range: < 1.20.6-r10
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.21.5-r6
- (no CPE)range: < 1.22.2-r1
- (no CPE)range: < 1.22.2-r1
- (no CPE)range: < 1.22.2-r1
- (no CPE)range: >= 1.4.1, < 1.20.0
- (no CPE)range: >= 1.4.1, < 1.20.0
- (no CPE)range: < 0.0.20241104T154416-150000.1.12.1
- (no CPE)range: < 0.0.20241104T154416-150000.1.12.1
- (no CPE)range: < 0.0.20241104T154416-1.1
- (no CPE)range: < 0.0.20241104T154416-150000.1.12.1
- (no CPE)range: < 0.0.20241104T154416-150000.1.12.1
- (no CPE)range: < 0.0.20241104T154416-5.1
- HashiCorp/Consulv5Range: 1.4.1
- HashiCorp/Consul Enterprisev5Range: 1.4.1
Patches
107fae7bb0be8[Security] Fix XSS Vulnerability where content-type header wasn't explicitly set (#21704)
3 files changed · +65 −5
agent/http.go+34 −1 modified@@ -6,6 +6,7 @@ package agent import ( "encoding/json" "fmt" + "github.com/hashicorp/go-hclog" "io" "net" "net/http" @@ -43,6 +44,11 @@ import ( "github.com/hashicorp/consul/proto/private/pbcommon" ) +const ( + contentTypeHeader = "Content-Type" + plainContentType = "text/plain; charset=utf-8" +) + var HTTPSummaries = []prometheus.SummaryDefinition{ { Name: []string{"api", "http"}, @@ -220,6 +226,7 @@ func (s *HTTPHandlers) handler() http.Handler { // If enableDebug register wrapped pprof handlers if !s.agent.enableDebug.Load() && s.checkACLDisabled() { resp.WriteHeader(http.StatusNotFound) + resp.Header().Set(contentTypeHeader, plainContentType) return } @@ -228,6 +235,7 @@ func (s *HTTPHandlers) handler() http.Handler { authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, nil, nil) if err != nil { + resp.Header().Set(contentTypeHeader, plainContentType) resp.WriteHeader(http.StatusForbidden) return } @@ -237,6 +245,7 @@ func (s *HTTPHandlers) handler() http.Handler { // TODO(partitions): should this be possible in a partition? // TODO(acl-error-enhancements): We should return error details somehow here. if authz.OperatorRead(nil) != acl.Allow { + resp.Header().Set(contentTypeHeader, plainContentType) resp.WriteHeader(http.StatusForbidden) return } @@ -317,6 +326,8 @@ func (s *HTTPHandlers) handler() http.Handler { } h = withRemoteAddrHandler(h) + h = ensureContentTypeHeader(h, s.agent.logger) + s.h = &wrappedMux{ mux: mux, handler: h, @@ -337,6 +348,20 @@ func withRemoteAddrHandler(next http.Handler) http.Handler { }) } +// Injects content type explicitly if not already set into response to prevent XSS +func ensureContentTypeHeader(next http.Handler, logger hclog.Logger) http.Handler { + + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + next.ServeHTTP(resp, req) + + val := resp.Header().Get(contentTypeHeader) + if val == "" { + resp.Header().Set(contentTypeHeader, plainContentType) + logger.Debug("warning: content-type header not explicitly set.", "request-path", req.URL) + } + }) +} + // nodeName returns the node name of the agent func (s *HTTPHandlers) nodeName() string { return s.agent.config.NodeName @@ -380,6 +405,8 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc "from", req.RemoteAddr, "error", err, ) + //set response type to plain to prevent XSS + resp.Header().Set(contentTypeHeader, plainContentType) resp.WriteHeader(http.StatusInternalServerError) return } @@ -406,6 +433,8 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc "from", req.RemoteAddr, "error", errMsg, ) + //set response type to plain to prevent XSS + resp.Header().Set(contentTypeHeader, plainContentType) resp.WriteHeader(http.StatusForbidden) fmt.Fprint(resp, errMsg) return @@ -585,6 +614,8 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc resp.Header().Add("X-Consul-Reason", errPayload.Reason) } } else { + //set response type to plain to prevent XSS + resp.Header().Set(contentTypeHeader, plainContentType) handleErr(err) return } @@ -596,6 +627,8 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc if contentType == "application/json" { buf, err = s.marshalJSON(req, obj) if err != nil { + //set response type to plain to prevent XSS + resp.Header().Set(contentTypeHeader, plainContentType) handleErr(err) return } @@ -606,7 +639,7 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc } } } - resp.Header().Set("Content-Type", contentType) + resp.Header().Set(contentTypeHeader, contentType) resp.WriteHeader(httpCode) resp.Write(buf) }
agent/http_test.go+28 −4 modified@@ -639,14 +639,14 @@ func TestHTTPAPIResponseHeaders(t *testing.T) { `) defer a.Shutdown() - requireHasHeadersSet(t, a, "/v1/agent/self") + requireHasHeadersSet(t, a, "/v1/agent/self", "application/json") // Check the Index page that just renders a simple message with UI disabled // also gets the right headers. - requireHasHeadersSet(t, a, "/") + requireHasHeadersSet(t, a, "/", "text/plain; charset=utf-8") } -func requireHasHeadersSet(t *testing.T, a *TestAgent, path string) { +func requireHasHeadersSet(t *testing.T, a *TestAgent, path string, contentType string) { t.Helper() resp := httptest.NewRecorder() @@ -661,6 +661,9 @@ func requireHasHeadersSet(t *testing.T, a *TestAgent, path string) { require.Equal(t, "1; mode=block", hdrs.Get("X-XSS-Protection"), "X-XSS-Protection header value incorrect") + + require.Equal(t, contentType, hdrs.Get("Content-Type"), + "") } func TestUIResponseHeaders(t *testing.T) { @@ -680,7 +683,28 @@ func TestUIResponseHeaders(t *testing.T) { `) defer a.Shutdown() - requireHasHeadersSet(t, a, "/ui") + //response header for the UI appears to be being handled by the UI itself. + requireHasHeadersSet(t, a, "/ui", "text/plain; charset=utf-8") +} + +func TestErrorContentTypeHeaderSet(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, ` + http_config { + response_headers = { + "Access-Control-Allow-Origin" = "*" + "X-XSS-Protection" = "1; mode=block" + "X-Frame-Options" = "SAMEORIGIN" + } + } + `) + defer a.Shutdown() + + requireHasHeadersSet(t, a, "/fake-path-doesn't-exist", "text/plain; charset=utf-8") } func TestAcceptEncodingGzip(t *testing.T) {
.changelog/21704.txt+3 −0 added@@ -0,0 +1,3 @@ +```release-note:security +Explicitly set 'Content-Type' header to mitigate XSS vulnerability. +``` \ No newline at end of file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-99wr-c2px-grmhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-10086ghsaADVISORY
- discuss.hashicorp.com/t/hcsec-2024-24-consul-vulnerable-to-reflected-xss-on-content-type-error-manipulationghsaWEB
- github.com/hashicorp/consul/commit/07fae7bb0be8593cc98c38b1ef4a49ed9188932fghsaWEB
- security.netapp.com/advisory/ntap-20250110-0006ghsaWEB
News mentions
0No linked articles in our index yet.