CVE-2022-40716
Description
HashiCorp Consul and Consul Enterprise up to 1.11.8, 1.12.4, and 1.13.1 do not check for multiple SAN URI values in a CSR on the internal RPC endpoint, enabling leverage of privileged access to bypass service mesh intentions. Fixed in 1.11.9, 1.12.5, and 1.13.2."
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
HashiCorp Consul fails to validate multiple SAN URI values in CSRs on the internal RPC endpoint, allowing attackers with privileged access to bypass service mesh intentions.
Vulnerability
Analysis
CVE-2022-40716 affects HashiCorp Consul and Consul Enterprise versions prior to 1.11.9, 1.12.5, and 1.13.2. The vulnerability lies in the internal RPC endpoint of the certificate signing process. When a Certificate Signing Request (CSR) is submitted for signing, the software does not validate that the CSR contains only a single Subject Alternative Name (SAN) URI value [1][3]. This oversight allows a CSR to include multiple SAN URIs, which is against the expected behavior for SPIFFE-based identities in a service mesh [4].
Exploitation
Exploitation requires privileged access to Consul, as the attacker must be able to issue CSRs to the internal RPC endpoint. With this level of access, an attacker can craft a CSR containing multiple SAN URIs. The system will then sign a certificate that includes multiple identities (e.g., a previously authorized service identity and a target service identity). This bypasses the service mesh intention checks that rely on single-identity certificates for access control [1]. The attack does not require network position changes but leverages existing administrative capabilities.
Impact
A successful attack allows an attacker to bypass service mesh intentions, which are Consul's mechanism for defining which services can communicate. An attacker with privileged access could potentially connect to services they should not have access to, effectively bypassing security policies enforced by the service mesh. This could lead to unauthorized data access, service disruption, or lateral movement within the mesh [3].
Mitigation
HashiCorp has released fixed versions: Consul 1.11.9, 1.12.5, and 1.13.2 [2][3]. The fix introduces a validation check that rejects CSRs with multiple SAN URIs, ensuring that each signed certificate represents only one identity [1][4]. Users should upgrade to these patched versions. There are no known workarounds; upgrading is the recommended mitigation.
AI Insight generated on May 21, 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.11.9 | 1.11.9 |
github.com/hashicorp/consulGo | >= 1.12.0, < 1.12.5 | 1.12.5 |
github.com/hashicorp/consulGo | >= 1.13.0, < 1.13.2 | 1.13.2 |
Affected products
29- HashiCorp/Consuldescription
- osv-coords28 versionspkg:apk/chainguard/consul-1.15pkg:apk/chainguard/consul-1.15-oci-entrypointpkg:apk/chainguard/consul-1.15-oci-entrypoint-compatpkg:apk/chainguard/consul-1.16pkg:apk/chainguard/consul-1.16-oci-entrypointpkg:apk/chainguard/consul-1.16-oci-entrypoint-compatpkg:apk/chainguard/consul-1.17pkg:apk/chainguard/consul-1.17-fipspkg:apk/chainguard/consul-1.17-fips-oci-entrypointpkg:apk/chainguard/consul-1.17-fips-oci-entrypoint-compatpkg:apk/chainguard/consul-1.17-oci-entrypointpkg:apk/chainguard/consul-1.17-oci-entrypoint-compatpkg:apk/chainguard/k3dpkg:apk/chainguard/k3d-proxypkg:apk/chainguard/k3d-toolspkg:apk/chainguard/traefikpkg:apk/wolfi/consul-1.15pkg:apk/wolfi/consul-1.15-oci-entrypointpkg:apk/wolfi/consul-1.15-oci-entrypoint-compatpkg:apk/wolfi/consul-1.16pkg:apk/wolfi/consul-1.16-oci-entrypointpkg:apk/wolfi/consul-1.16-oci-entrypoint-compatpkg:apk/wolfi/k3dpkg:apk/wolfi/k3d-proxypkg:apk/wolfi/k3d-toolspkg:apk/wolfi/traefikpkg:bitnami/consulpkg:golang/github.com/hashicorp/consul
< 0+ 27 more
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 2.9.8-r1
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 5.6.0-r11
- (no CPE)range: < 2.9.8-r1
- (no CPE)range: < 1.11.9
- (no CPE)range: < 1.11.9
Patches
18f6fb4f6fe94Backport #14579 to 1.13
5 files changed · +281 −7
agent/consul/auto_config_endpoint.go+6 −3 modified@@ -394,9 +394,12 @@ func parseAutoConfigCSR(csr string) (*x509.CertificateRequest, *connect.SpiffeID return nil, nil, fmt.Errorf("Failed to parse CSR: %w", err) } - // ensure that a URI SAN is present - if len(x509CSR.URIs) < 1 { - return nil, nil, fmt.Errorf("CSR didn't include any URI SANs") + // ensure that exactly one URI SAN is present + if len(x509CSR.URIs) != 1 { + return nil, nil, fmt.Errorf("CSR SAN contains an invalid number of URIs: %v", len(x509CSR.URIs)) + } + if len(x509CSR.EmailAddresses) > 0 { + return nil, nil, fmt.Errorf("CSR SAN does not allow specifying email addresses") } // Parse the SPIFFE ID
agent/consul/auto_config_endpoint_test.go+125 −0 modified@@ -1,12 +1,17 @@ package consul import ( + "bytes" + "crypto" + crand "crypto/rand" "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "io/ioutil" "math/rand" "net" + "net/url" "path" "testing" "time" @@ -874,6 +879,126 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) { backend.AssertExpectations(t) } +func TestAutoConfig_parseAutoConfigCSR(t *testing.T) { + // createCSR copies the behavior of connect.CreateCSR with some + // customizations to allow for better unit testing. + createCSR := func(tmpl *x509.CertificateRequest, privateKey crypto.Signer) (string, error) { + connect.HackSANExtensionForCSR(tmpl) + bs, err := x509.CreateCertificateRequest(crand.Reader, tmpl, privateKey) + require.NoError(t, err) + var csrBuf bytes.Buffer + err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) + require.NoError(t, err) + return csrBuf.String(), nil + } + pk, _, err := connect.GeneratePrivateKey() + require.NoError(t, err) + + agentURI := connect.SpiffeIDAgent{ + Host: "test-host", + Datacenter: "tdc1", + Agent: "test-agent", + }.URI() + + tests := []struct { + name string + setup func() string + expectErr string + }{ + { + name: "err_garbage_data", + expectErr: "Failed to parse CSR", + setup: func() string { return "garbage" }, + }, + { + name: "err_not_one_uri", + expectErr: "CSR SAN contains an invalid number of URIs", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI, agentURI}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_email", + expectErr: "CSR SAN does not allow specifying email addresses", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI}, + EmailAddresses: []string{"test@example.com"}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_spiffe_parse_uri", + expectErr: "Failed to parse the SPIFFE URI", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{connect.SpiffeIDAgent{}.URI()}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_not_agent", + expectErr: "SPIFFE ID is not an Agent ID", + setup: func() string { + spiffe := connect.SpiffeIDService{ + Namespace: "tns", + Datacenter: "tdc1", + Service: "test-service", + } + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{spiffe.URI()}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "success", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req, spif, err := parseAutoConfigCSR(tc.setup()) + if tc.expectErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + // TODO better verification of these + require.NotNil(t, req) + require.NotNil(t, spif) + } + + }) + } +} + func TestAutoConfig_invalidSegmentName(t *testing.T) { invalid := []string{ "\n",
agent/consul/leader_connect_ca.go+9 −4 modified@@ -1406,10 +1406,15 @@ func (l *connectSignRateLimiter) getCSRRateLimiterWithLimit(limit rate.Limit) *r // identified by the SPIFFE ID in the given CSR's SAN. It performs authorization // using the given acl.Authorizer. func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, authz acl.Authorizer) (*structs.IssuedCert, error) { - // Parse the SPIFFE ID from the CSR SAN. - if len(csr.URIs) == 0 { - return nil, connect.InvalidCSRError("CSR SAN does not contain a SPIFFE ID") + // Note that only one spiffe id is allowed currently. If more than one is desired + // in future implmentations, then each ID should have authorization checks. + if len(csr.URIs) != 1 { + return nil, connect.InvalidCSRError("CSR SAN contains an invalid number of URIs: %v", len(csr.URIs)) + } + if len(csr.EmailAddresses) > 0 { + return nil, connect.InvalidCSRError("CSR SAN does not allow specifying email addresses") } + // Parse the SPIFFE ID from the CSR SAN. spiffeID, err := connect.ParseCertURI(csr.URIs[0]) if err != nil { return nil, err @@ -1452,7 +1457,7 @@ func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, au "we are %s", v.Datacenter, dc) } default: - return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service or agent ID") + return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service, mesh-gateway, or agent ID") } return c.SignCertificate(csr, spiffeID)
agent/consul/leader_connect_ca_test.go+138 −0 modified@@ -24,6 +24,7 @@ import ( msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc" "github.com/hashicorp/consul-net-rpc/net/rpc" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/connect" ca "github.com/hashicorp/consul/agent/connect/ca" "github.com/hashicorp/consul/agent/consul/fsm" @@ -1042,3 +1043,140 @@ func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string, rootPEM require.NoError(t, err, "failed to set signed intermediate") return lib.EnsureTrailingNewline(buf.String()) } + +func TestCAManager_AuthorizeAndSignCertificate(t *testing.T) { + conf := DefaultConfig() + conf.PrimaryDatacenter = "dc1" + conf.Datacenter = "dc2" + manager := NewCAManager(nil, nil, testutil.Logger(t), conf) + + agentURL := connect.SpiffeIDAgent{ + Agent: "test-agent", + Datacenter: conf.PrimaryDatacenter, + Host: "test-host", + }.URI() + serviceURL := connect.SpiffeIDService{ + Datacenter: conf.PrimaryDatacenter, + Namespace: "ns1", + Service: "test-service", + }.URI() + meshURL := connect.SpiffeIDMeshGateway{ + Datacenter: conf.PrimaryDatacenter, + Host: "test-host", + Partition: "test-partition", + }.URI() + + tests := []struct { + name string + expectErr string + getCSR func() *x509.CertificateRequest + authAllow bool + }{ + { + name: "err_not_one_uri", + expectErr: "CSR SAN contains an invalid number of URIs", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL, agentURL}, + } + }, + }, + { + name: "err_email", + expectErr: "CSR SAN does not allow specifying email addresses", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL}, + EmailAddresses: []string{"test@example.com"}, + } + }, + }, + { + name: "err_invalid_spiffe_id", + expectErr: "SPIFFE ID is not in the expected format", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{connect.SpiffeIDAgent{}.URI()}, + } + }, + }, + { + name: "err_service_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{serviceURL}, + } + }, + }, + { + name: "err_service_different_dc", + expectErr: "SPIFFE ID in CSR from a different datacenter", + authAllow: true, + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{serviceURL}, + } + }, + }, + { + name: "err_agent_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL}, + } + }, + }, + { + name: "err_meshgw_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{meshURL}, + } + }, + }, + { + name: "err_meshgw_different_dc", + expectErr: "SPIFFE ID in CSR from a different datacenter", + authAllow: true, + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{meshURL}, + } + }, + }, + { + name: "err_invalid_spiffe_type", + expectErr: "SPIFFE ID in CSR must be a service, mesh-gateway, or agent ID", + getCSR: func() *x509.CertificateRequest { + u := connect.SpiffeIDSigning{ + ClusterID: "test-cluster-id", + Domain: "test-domain", + }.URI() + return &x509.CertificateRequest{ + URIs: []*url.URL{u}, + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + authz := acl.DenyAll() + if tc.authAllow { + authz = acl.AllowAll() + } + + cert, err := manager.AuthorizeAndSignCertificate(tc.getCSR(), authz) + if tc.expectErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + require.NotNil(t, cert) + } + }) + } +}
.changelog/14579.txt+3 −0 added@@ -0,0 +1,3 @@ +```release-note:security +connect: Added URI length checks to ConnectCA CSR requests. Prior to this change, it was possible for a malicious actor to designate multiple SAN URI values in a call to the `ConnectCA.Sign` endpoint. The endpoint now only allows for exactly one SAN URI to be specified. +``` \ 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
11- github.com/advisories/GHSA-m69r-9g56-7mv8ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LYZOKMMVX4SIEHPJW3SJUQGMO5YZCPHC/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/XNF4OLYZRQE75EB5TW5N42FSXHBXGWFE/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/ZTE4ITXXPIWZEQ4HYQCB6N6GZIMWXDAI/mitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2022-40716ghsaADVISORY
- discuss.hashicorp.comghsaWEB
- discuss.hashicorp.com/t/hcsec-2022-20-consul-service-mesh-intention-bypass-with-malicious-certificate-signing-request/44628ghsaWEB
- github.com/hashicorp/consul/commit/8f6fb4f6fe9488b8ec37da71ac503081d7d3760bghsaWEB
- github.com/hashicorp/consul/pull/14579ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/LYZOKMMVX4SIEHPJW3SJUQGMO5YZCPHCghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZTE4ITXXPIWZEQ4HYQCB6N6GZIMWXDAIghsaWEB
News mentions
0No linked articles in our index yet.