java-topology/defects/caddy/patch/caddy-0002-tls-automation-policy-subjects-dedup.md

4.5 KiB
Raw Blame History

UNDF: UNDF-2026-000000532

UNDF: (pending)

caddy-0002: consolidateAutomationPolicies — O(P²×S) slices.Contains in subject merge

CWE-407 — Algorithmic Complexity

Field Value
ID caddy-0002
Severity MEDIUM
Ecosystem Caddy
File caddyconfig/httpcaddyfile/tlsapp.go
Lines 737790 (function consolidateAutomationPolicies)
Complexity O(P²×S) where P = automation policies, S = subjects per policy
Hot path Caddyfile config reload / caddy reload

Defect

consolidateAutomationPolicies() merges TLS automation policies that share the same issuer/manager/storage settings. When combining subjects from two matching policies it uses slices.Contains inside a triply-nested loop:

// remove or combine duplicate policies
outer:
for i := 0; i < len(aps); i++ {
    for j := i + 1; j < len(aps); j++ {          // O(P²) outer × inner
        ...
        if reflect.DeepEqual(...) {
            ...
            } else {
                // avoid repeated subjects
                for _, subj := range aps[j].SubjectsRaw {
                    if !slices.Contains(aps[i].SubjectsRaw, subj) { // O(S) scan
                        aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
                    }
                }

The innermost slices.Contains(aps[i].SubjectsRaw, subj) scans the growing SubjectsRaw slice for each subject from aps[j]. With P policies each having S subjects, subject-merge has cost O(P × S²) — and the outer pair-loop adds another O(P²) factor. Overall O(P²×S).

This runs at every Caddyfile reload. A site with many virtual hosts and per-host TLS policies can have P=100 policies × S=50 subjects = 250,000 comparisons per reload.

Fix

Replace slices.Contains with a map[string]struct{} set built once per merge operation:

// avoid repeated subjects — O(S) build + O(1) lookup per subject
existing := make(map[string]struct{}, len(aps[i].SubjectsRaw))
for _, s := range aps[i].SubjectsRaw {
    existing[s] = struct{}{}
}
for _, subj := range aps[j].SubjectsRaw {
    if _, ok := existing[subj]; !ok {
        existing[subj] = struct{}{}
        aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
    }
}

Full patched section:

outer:
for i := 0; i < len(aps); i++ {
    for j := i + 1; j < len(aps); j++ {
        if reflect.DeepEqual(aps[i], aps[j]) {
            aps = slices.Delete(aps, j, j+1)
            i--
            continue outer
        }

        if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
            reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
            bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
            aps[i].MustStaple == aps[j].MustStaple &&
            aps[i].KeyType == aps[j].KeyType &&
            aps[i].OnDemand == aps[j].OnDemand &&
            aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
            aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {

            if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
                if automationPolicyShadows(i, aps) >= j {
                    aps = slices.Delete(aps, i, i+1)
                    i--
                    continue outer
                }
            } else {
                // CWE-407 fix: O(S) set-based dedup instead of O(S²) scan
                existing := make(map[string]struct{}, len(aps[i].SubjectsRaw))
                for _, s := range aps[i].SubjectsRaw {
                    existing[s] = struct{}{}
                }
                for _, subj := range aps[j].SubjectsRaw {
                    if _, ok := existing[subj]; !ok {
                        existing[subj] = struct{}{}
                        aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
                    }
                }
                aps = slices.Delete(aps, j, j+1)
                j--
            }
        }
    }
}

Speedup

Policies (P) Subjects/policy (S) Before After Ratio
10 10 ~1,000 comparisons ~100 map ops 10×
50 50 ~125,000 ~2,500 50×
100 100 ~1,000,000 ~10,000 100×

Typical Caddy deployments with large virtual-host configs (50+ sites, each with its own TLS policy) will see ~50× speedup on reload.

Notes

automationPolicyIsSubset() at line 795 also uses slices.ContainsFunc in an O(A×B) double-loop over subjects, and is a secondary site for the same fix (replace with a map[string]struct{} for policy b's subjects).