VYPR
High severityNVD Advisory· Published Nov 15, 2022· Updated Apr 29, 2025

Consul Peering Imported Nodes/Services Leak

CVE-2022-3920

Description

HashiCorp Consul and Consul Enterprise 1.13.0 up to 1.13.3 do not filter cluster filtering's imported nodes and services for HTTP or RPC endpoints used by the UI. Fixed in 1.14.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

HashiCorp Consul 1.13.0-1.13.3 leaks imported cluster peers' nodes and services via UI endpoints, fixed in 1.14.0.

Vulnerability

Analysis

HashiCorp Consul and Consul Enterprise versions 1.13.0 through 1.13.3 fail to properly filter ACLs for imported nodes and services when handling HTTP and RPC endpoints used by the UI. Specifically, the /v1/internal/ui/nodes and /v1/internal/ui/services endpoints do not apply cluster peering ACL filters, exposing information from peer clusters [1][2].

Attack

Vector

An attacker does not require an ACL token to exploit this vulnerability. The cluster peering feature, introduced as a beta in Consul 1.13.0, allows nodes and services from peered clusters to be viewed without proper authorization. By accessing the UI or directly calling the affected endpoints, an attacker can enumerate imported resources [3].

Impact

Successful exploitation leads to unauthorized disclosure of node and service information from peered clusters. This can include service names, node identities, and health check details, potentially aiding further attacks or exposing sensitive infrastructure [2][3].

Mitigation

The vulnerability is fixed in Consul 1.14.0. Users are advised to upgrade immediately, especially those using the cluster peering feature. No workarounds are available [1][2][3].

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.

PackageAffected versionsPatched versions
github.com/hashicorp/consulGo
>= 1.13.0, < 1.14.01.14.0

Affected products

10

Patches

1
706866fa0016

Ensure that NodeDump imported nodes are filtered (#15356)

https://github.com/hashicorp/consulFreddyNov 14, 2022via ghsa
4 files changed · +241 45
  • agent/structs/aclfilter/filter.go+6 1 modified
    @@ -61,7 +61,12 @@ func (f *Filter) Filter(subject any) {
     		v.QueryMeta.ResultsFilteredByACLs = f.filterIntentions(&v.Intentions)
     
     	case *structs.IndexedNodeDump:
    -		v.QueryMeta.ResultsFilteredByACLs = f.filterNodeDump(&v.Dump)
    +		if f.filterNodeDump(&v.Dump) {
    +			v.QueryMeta.ResultsFilteredByACLs = true
    +		}
    +		if f.filterNodeDump(&v.ImportedDump) {
    +			v.QueryMeta.ResultsFilteredByACLs = true
    +		}
     
     	case *structs.IndexedServiceDump:
     		v.QueryMeta.ResultsFilteredByACLs = f.filterServiceDump(&v.Dump)
    
  • agent/structs/aclfilter/filter_test.go+229 43 modified
    @@ -1444,78 +1444,264 @@ func TestACL_filterNodeDump(t *testing.T) {
     					},
     				},
     			},
    +			ImportedDump: structs.NodeDump{
    +				{
    +					// The node and service names are intentionally the same to ensure that
    +					// local permissions for the same names do not allow reading imports.
    +					Node:     "node1",
    +					PeerName: "cluster-02",
    +					Services: []*structs.NodeService{
    +						{
    +							ID:       "foo",
    +							Service:  "foo",
    +							PeerName: "cluster-02",
    +						},
    +					},
    +					Checks: []*structs.HealthCheck{
    +						{
    +							Node:        "node1",
    +							CheckID:     "check1",
    +							ServiceName: "foo",
    +							PeerName:    "cluster-02",
    +						},
    +					},
    +				},
    +			},
     		}
     	}
    +	type testCase struct {
    +		authzFn func() acl.Authorizer
    +		expect  *structs.IndexedNodeDump
    +	}
     
    -	t.Run("allowed", func(t *testing.T) {
    +	run := func(t *testing.T, tc testCase) {
    +		authz := tc.authzFn()
     
    -		policy, err := acl.NewPolicyFromSource(`
    +		list := makeList()
    +		New(authz, logger).Filter(list)
    +
    +		require.Equal(t, tc.expect, list)
    +	}
    +
    +	tt := map[string]testCase{
    +		"denied": {
    +			authzFn: func() acl.Authorizer {
    +				return acl.DenyAll()
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump:         structs.NodeDump{},
    +				ImportedDump: structs.NodeDump{},
    +				QueryMeta:    structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read local service but not the node": {
    +			authzFn: func() acl.Authorizer {
    +				policy, err := acl.NewPolicyFromSource(`
     			service "foo" {
     			  policy = "read"
     			}
    +		`, acl.SyntaxLegacy, nil, nil)
    +				require.NoError(t, err)
    +
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
    +
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump:         structs.NodeDump{},
    +				ImportedDump: structs.NodeDump{},
    +				QueryMeta:    structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read the local node but not the service": {
    +			authzFn: func() acl.Authorizer {
    +				policy, err := acl.NewPolicyFromSource(`
     			node "node1" {
     			  policy = "read"
     			}
     		`, acl.SyntaxLegacy, nil, nil)
    -		require.NoError(t, err)
    -
    -		authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    -		require.NoError(t, err)
    -
    -		list := makeList()
    -		New(authz, logger).Filter(list)
    -
    -		require.Len(t, list.Dump, 1)
    -		require.False(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
    -	})
    +				require.NoError(t, err)
     
    -	t.Run("allowed to read the service, but not the node", func(t *testing.T) {
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
     
    -		policy, err := acl.NewPolicyFromSource(`
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump: structs.NodeDump{
    +					{
    +						Node:     "node1",
    +						Services: []*structs.NodeService{},
    +						Checks:   structs.HealthChecks{},
    +					},
    +				},
    +				ImportedDump: structs.NodeDump{},
    +				QueryMeta:    structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read local data": {
    +			authzFn: func() acl.Authorizer {
    +				policy, err := acl.NewPolicyFromSource(`
     			service "foo" {
     			  policy = "read"
     			}
    +			node "node1" {
    +			  policy = "read"
    +			}
     		`, acl.SyntaxLegacy, nil, nil)
    -		require.NoError(t, err)
    -
    -		authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    -		require.NoError(t, err)
    +				require.NoError(t, err)
     
    -		list := makeList()
    -		New(authz, logger).Filter(list)
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
     
    -		require.Empty(t, list.Dump)
    -		require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
    -	})
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump: structs.NodeDump{
    +					{
    +						Node: "node1",
    +						Services: []*structs.NodeService{
    +							{
    +								ID:      "foo",
    +								Service: "foo",
    +							},
    +						},
    +						Checks: []*structs.HealthCheck{
    +							{
    +								Node:        "node1",
    +								CheckID:     "check1",
    +								ServiceName: "foo",
    +							},
    +						},
    +					},
    +				},
    +				ImportedDump: structs.NodeDump{},
    +				QueryMeta:    structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read imported service but not the node": {
    +			authzFn: func() acl.Authorizer {
    +				// Wildcard service read also grants read to imported services.
    +				policy, err := acl.NewPolicyFromSource(`
    +			service "" {
    +			  policy = "read"
    +			}
    +		`, acl.SyntaxLegacy, nil, nil)
    +				require.NoError(t, err)
     
    -	t.Run("allowed to read the node, but not the service", func(t *testing.T) {
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
     
    -		policy, err := acl.NewPolicyFromSource(`
    -			node "node1" {
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump:         structs.NodeDump{},
    +				ImportedDump: structs.NodeDump{},
    +				QueryMeta:    structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read the imported node but not the service": {
    +			authzFn: func() acl.Authorizer {
    +				// Wildcard node read also grants read to imported nodes.
    +				policy, err := acl.NewPolicyFromSource(`
    +			node "" {
     			  policy = "read"
     			}
     		`, acl.SyntaxLegacy, nil, nil)
    -		require.NoError(t, err)
    -
    -		authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    -		require.NoError(t, err)
    +				require.NoError(t, err)
     
    -		list := makeList()
    -		New(authz, logger).Filter(list)
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
     
    -		require.Len(t, list.Dump, 1)
    -		require.Empty(t, list.Dump[0].Services)
    -		require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
    -	})
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump: structs.NodeDump{
    +					{
    +						Node:     "node1",
    +						Services: []*structs.NodeService{},
    +						Checks:   structs.HealthChecks{},
    +					},
    +				},
    +				ImportedDump: structs.NodeDump{
    +					{
    +						Node:     "node1",
    +						PeerName: "cluster-02",
    +						Services: []*structs.NodeService{},
    +						Checks:   structs.HealthChecks{},
    +					},
    +				},
    +				QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: true},
    +			},
    +		},
    +		"can read all data": {
    +			authzFn: func() acl.Authorizer {
    +				policy, err := acl.NewPolicyFromSource(`
    +			service "" {
    +			  policy = "read"
    +			}
    +			node "" {
    +			  policy = "read"
    +			}
    +		`, acl.SyntaxLegacy, nil, nil)
    +				require.NoError(t, err)
     
    -	t.Run("denied", func(t *testing.T) {
    +				authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
    +				require.NoError(t, err)
     
    -		list := makeList()
    -		New(acl.DenyAll(), logger).Filter(list)
    +				return authz
    +			},
    +			expect: &structs.IndexedNodeDump{
    +				Dump: structs.NodeDump{
    +					{
    +						Node: "node1",
    +						Services: []*structs.NodeService{
    +							{
    +								ID:      "foo",
    +								Service: "foo",
    +							},
    +						},
    +						Checks: []*structs.HealthCheck{
    +							{
    +								Node:        "node1",
    +								CheckID:     "check1",
    +								ServiceName: "foo",
    +							},
    +						},
    +					},
    +				},
    +				ImportedDump: structs.NodeDump{
    +					{
    +						Node:     "node1",
    +						PeerName: "cluster-02",
    +						Services: []*structs.NodeService{
    +							{
    +								ID:       "foo",
    +								Service:  "foo",
    +								PeerName: "cluster-02",
    +							},
    +						},
    +						Checks: []*structs.HealthCheck{
    +							{
    +								Node:        "node1",
    +								CheckID:     "check1",
    +								ServiceName: "foo",
    +								PeerName:    "cluster-02",
    +							},
    +						},
    +					},
    +				},
    +				QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: false},
    +			},
    +		},
    +	}
     
    -		require.Empty(t, list.Dump)
    -		require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
    -	})
    +	for name, tc := range tt {
    +		t.Run(name, func(t *testing.T) {
    +			run(t, tc)
    +		})
    +	}
     }
     
     func TestACL_filterNodes(t *testing.T) {
    
  • agent/structs/structs_oss.go+3 1 modified
    @@ -67,7 +67,9 @@ func (n *Node) OverridePartition(_ string) {
     
     func (_ *Coordinate) FillAuthzContext(_ *acl.AuthorizerContext) {}
     
    -func (_ *NodeInfo) FillAuthzContext(_ *acl.AuthorizerContext) {}
    +func (n *NodeInfo) FillAuthzContext(ctx *acl.AuthorizerContext) {
    +	ctx.Peer = n.PeerName
    +}
     
     // FillAuthzContext stub
     func (_ *DirEntry) FillAuthzContext(_ *acl.AuthorizerContext) {}
    
  • .changelog/15356.txt+3 0 added
    @@ -0,0 +1,3 @@
    +```release-note:security
    +Ensure that data imported from peers is filtered by ACLs at the UI Nodes/Services endpoints [CVE-2022-3920](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-3920)
    +```
    \ 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

4

News mentions

0

No linked articles in our index yet.