java-topology/defects/vault/patch/vault-0001-identity-group-membership-linear-scan.md

3.1 KiB

UNDF: UNDF-2026-000000569

vault-0001 — HashiCorp Vault: sanitizeAndUpsertGroup O(G²) group membership test

Severity: HIGH CWE: CWE-407 (Algorithmic Complexity — Linear Membership Test in Loop) File: vault/identity_store_util.go Function: (*IdentityStore).sanitizeAndUpsertGroup

Defect

// identity_store_util.go:2317-2318
for _, currentMemberGroupID := range currentMemberGroupIDs {   // O(G_current)
    if strutil.StrListContains(memberGroupIDs, currentMemberGroupID) {  // O(G_member)
        continue
    }
    // ... remove from parent group
}

strutil.StrListContains performs a linear scan of the memberGroupIDs slice:

// sdk/helper/strutil/strutil.go
func StrListContains(haystack []string, needle string) bool {
    for _, item := range haystack {
        if item == needle { return true }
    }
    return false
}

When updating a group with G member groups, the outer loop runs G times and each iteration scans the full memberGroupIDs slice (also up to G entries). Total: O(G²).

This function is called on every PUT /identity/group/:id request. Large Vault deployments with enterprise customers frequently have groups with hundreds to thousands of members (LDAP sync, external IdP groups). At G=1000 this is 1,000,000 string comparisons per update.

Fix

Pre-build a map[string]bool from memberGroupIDs before the loop for O(1) lookups:

// Build O(1) lookup set before the loop
memberGroupIDSet := make(map[string]bool, len(memberGroupIDs))
for _, id := range memberGroupIDs {
    memberGroupIDSet[id] = true
}

for _, currentMemberGroupID := range currentMemberGroupIDs {
    if memberGroupIDSet[currentMemberGroupID] {    // O(1)
        continue
    }
    // ... remove from parent group
}

Patch

--- a/vault/identity_store_util.go
+++ b/vault/identity_store_util.go
@@ -2301,6 +2301,12 @@ func (i *IdentityStore) sanitizeAndUpsertGroup(...) error {
 	memberGroupIDs = strutil.RemoveDuplicates(memberGroupIDs, false)

+	// CWE-407: pre-build a set for O(1) membership checks below.
+	memberGroupIDSet := make(map[string]bool, len(memberGroupIDs))
+	for _, id := range memberGroupIDs {
+		memberGroupIDSet[id] = true
+	}
+
 	// For those group member IDs that are removed from the list, remove current
 	// group ID as their respective ParentGroupID.
@@ -2317,7 +2323,7 @@ func (i *IdentityStore) sanitizeAndUpsertGroup(...) error {
 	for _, currentMemberGroupID := range currentMemberGroupIDs {
-		if strutil.StrListContains(memberGroupIDs, currentMemberGroupID) {
+		if memberGroupIDSet[currentMemberGroupID] {
 			continue
 		}

Complexity

G (member groups) Before (comparisons) After (comparisons)
100 10,000 100
500 250,000 500
1,000 1,000,000 1,000
5,000 25,000,000 5,000

Reproduction

See defects/vault/unit/VaultGroupMembershipAlgorithm.java — ratio ≥ 5x at G=400 (72x at G=1000).