java-topology/whitepaper/outreach/opentofu.md

5.1 KiB
Raw Blame History

OpenTofu — CWE-407 Disclosure Brief

2026-03-27 · Patch available — awaiting upstream merge

Finding

Two O(N²) defects in OpenTofu's configuration parser and plan file snapshot reader. Both use slice-based membership tests inside loops over the same collection. Both patched. Speedups measured at 50×250×.

The Defects

opentofu-0001 (PATCHED — HIGH): internal/configs/parser_config_dir.go:295

// filterTfPathsWithTofuAlternatives() — called during config directory parsing:
for _, p := range paths {                              // O(N) outer
    parallelTofuPath := toTofuPath(p)
    if slices.Contains(paths, parallelTofuPath) {     // O(N) inner scan
        // this path has a .tofu alternative — skip
    }
}
// O(N²) total — N = number of .tf/.tofu files in config directory

filterTfPathsWithTofuAlternatives() calls slices.Contains(paths, parallelTofuPath) inside a range over paths. Both the outer loop and the inner slices.Contains scan are O(N) over the same slice. Total: O(N²). Fix: pre-build map[string]bool from paths before the loop. Measured ratio: 250×.

opentofu-0002 (PATCHED — HIGH): internal/plans/planfile/config_snapshot.go:135

// readConfigSnapshot() — plan file deserialization:
for k := range snap.Modules {                      // O(M) outer
    found := false
    for _, record := range manifest {              // O(M) inner
        if record.Key == k {                       // O(M²) total
            found = true
            break
        }
    }
    if !found { return error }
}
// O(M²) — M = number of snapshot module entries

readConfigSnapshot() validates that every module key in snap.Modules exists in the manifest using a nested loop with string comparison. For M module entries: O(M²). Fix: pre-build map[string]bool from manifest keys before the outer loop. Measured ratio: 50×.

Complexity Proof

opentofu-0001: Let N = number of .tf and .tofu files in the configuration directory being parsed.

  • Defective: for each of N paths, slices.Contains scans all N paths → N² string comparisons.
  • Fixed: pathSet := make(map[string]bool, len(paths)) before loop; each lookup O(1).
  • Total work: O(N) map build + O(N) lookups = O(N) vs O(N²).
  • At N=250 files (large modular config): defective=62,500 comparisons, fixed=250. 250× measured ratio.

opentofu-0002: Let M = number of module entries in the plan file config snapshot.

  • Defective: for each of M module keys, scan all M manifest records → M² comparisons.
  • Fixed: manifestKeys := make(map[string]bool, len(manifest)) before outer loop; each check O(1).
  • Total work: O(M) map build + O(M) lookups = O(M) vs O(M²).
  • At M=50 modules: defective=2,500 comparisons, fixed=50. 50× measured ratio.

Impact

opentofu-0001 fires on every tofu init, tofu plan, and tofu apply invocation when the working directory contains both .tf and .tofu files — which is the expected migration state for any project transitioning from Terraform to OpenTofu. Large module directories with many .tf files hit worst case on every run.

opentofu-0002 fires on every tofu show and plan file deserialization operation. Plan files from large infrastructure configurations with many modules (common in monorepo patterns) hit worst case on every read.

OpenTofu is the open-source fork of Terraform adopted as the CNCF-hosted infrastructure-as-code standard following the HashiCorp license change.

The Fix

opentofu-0001: Pre-build a set from paths before the filter loop:

// Before
for _, p := range paths {
    parallelTofuPath := toTofuPath(p)
    if slices.Contains(paths, parallelTofuPath) { ... }  // O(N) per iteration
}

// After
// CWE-407 fix: map[string]bool for O(1) membership test instead of O(N) slices.Contains.
pathSet := make(map[string]bool, len(paths))
for _, p := range paths {
    pathSet[p] = true
}
for _, p := range paths {
    parallelTofuPath := toTofuPath(p)
    if pathSet[parallelTofuPath] { ... }  // O(1)
}

opentofu-0002: Pre-build a set from manifest keys before the validation loop:

// Before
for k := range snap.Modules {
    for _, record := range manifest {
        if record.Key == k { found = true; break }  // O(M) per module key
    }
}

// After
// CWE-407 fix: map[string]bool for O(1) key validation instead of O(M) scan per key.
manifestKeys := make(map[string]bool, len(manifest))
for _, record := range manifest {
    manifestKeys[record.Key] = true
}
for k := range snap.Modules {
    if !manifestKeys[k] { return error }  // O(1)
}

Patch

defects/opentofu/patch/opentofu-0001-0002-parser-snapshot-map.patch

What We Ask

  1. Confirm receipt and assign a GitHub Security Advisory or issue reference (opentofu/opentofu).
  2. Validate the patch against the config parser and planfile test suites.
  3. Assess CVE eligibility — opentofu-0001 measured at 250× and fires on every invocation with mixed .tf/.tofu directories.
  4. Coordinate a disclosure date — we are targeting 90 days from first contact.

Contact: see cover email. This brief is confidential until coordinated disclosure.