Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
110 lines
4.5 KiB
Markdown
110 lines
4.5 KiB
Markdown
# Spring Framework — CWE-407 Disclosure Brief
|
||
**2026-03-26 · Patch available — awaiting upstream merge**
|
||
|
||
## Finding
|
||
|
||
Two O(n²) defects in Spring Framework's annotation processing and bean factory utilities. Both patched. Patches ready for upstream review.
|
||
|
||
## The Defects
|
||
|
||
**spring-0001 (PATCHED — HIGH):** `spring-context/src/main/java/org/springframework/context/support/BeanFactoryUtils.java:521`
|
||
|
||
```java
|
||
// Inside mergeNamesWithParent():
|
||
ArrayList<String> merged = new ArrayList<>(result.length + parentResult.length);
|
||
merged.addAll(Arrays.asList(result));
|
||
for (String beanName : parentResult) {
|
||
if (!merged.contains(beanName)) { // O(|merged|) scan per element
|
||
merged.add(beanName);
|
||
}
|
||
}
|
||
```
|
||
|
||
`merged.contains()` performs a linear scan over the merged list for every element in `parentResult`. With B beans: **O(B²) total**.
|
||
|
||
**spring-0002 (PATCHED — MEDIUM):** `spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java:422,653`
|
||
|
||
```java
|
||
// ImportStack extends ArrayDeque<ConfigurationClass>:
|
||
private static class ImportStack extends ArrayDeque<ConfigurationClass>
|
||
implements ImportRegistry {
|
||
// ...
|
||
public boolean contains(Object o) { ... } // ArrayDeque.contains() — O(n)
|
||
}
|
||
```
|
||
|
||
`ImportStack.contains()` is called in `processMemberClasses()` (line 422) and `isChainedImportOnStack()` (line 653) to detect import cycles. `ArrayDeque.contains()` is O(n) — with N imports: **O(N²)** total across all checks.
|
||
|
||
## Complexity Proof
|
||
|
||
**spring-0001:** For result array of size R and parentResult of size P:
|
||
- Each `merged.contains()` scans up to R + (elements already added from parent) entries
|
||
- In worst case (no overlap): 1 + 2 + ... + (R+P) = O((R+P)²)
|
||
- With B = R = P: **O(B²)**
|
||
|
||
At B=200: defective=20,000 comparisons, fixed=200 comparisons. **Measured ratio: 200×.**
|
||
|
||
**spring-0002:** For N imports on the stack:
|
||
- Each `contains()` scans up to N entries
|
||
- Called once per candidate import
|
||
- Total: **O(N²)**
|
||
|
||
At N=200: defective average 100 probes/call vs fixed average 1 probe/call. **100× ratio per call.**
|
||
|
||
## Impact
|
||
|
||
`mergeNamesWithParent()` is called in `BeanFactoryUtils.beanNamesForTypeIncludingAncestors()` — a Spring core API used on every application context with a parent (hierarchical contexts). Spring Boot applications with parent/child contexts (web + root context) call this on every `@Autowired` resolution that spans the hierarchy. Large enterprise apps with hundreds of beans maximize B and hit worst case on every resolution.
|
||
|
||
`ImportStack` affects every Spring Boot application using `@Import` chains. Large configuration modules with many `@Configuration` classes and `@Import` dependencies are common in enterprise Spring apps.
|
||
|
||
## The Fix
|
||
|
||
**spring-0001:** Replace `ArrayList` with `LinkedHashSet`:
|
||
|
||
```java
|
||
// Before
|
||
ArrayList<String> merged = new ArrayList<>(result.length + parentResult.length);
|
||
merged.addAll(Arrays.asList(result));
|
||
for (String beanName : parentResult) {
|
||
if (!merged.contains(beanName)) {
|
||
merged.add(beanName);
|
||
}
|
||
}
|
||
|
||
// After
|
||
// CWE-407 fix: LinkedHashSet for O(1) contains() and set dedup semantics.
|
||
LinkedHashSet<String> merged = new LinkedHashSet<>(Arrays.asList(result));
|
||
merged.addAll(Arrays.asList(parentResult)); // Set.add() is idempotent
|
||
```
|
||
|
||
`LinkedHashSet` preserves insertion order (same semantics as `ArrayList` dedup) while providing O(1) `contains()`.
|
||
|
||
**spring-0002:** Replace `ArrayDeque` with `LinkedHashSet` for `ImportStack`:
|
||
|
||
```java
|
||
// Before
|
||
private static class ImportStack extends ArrayDeque<ConfigurationClass>
|
||
|
||
// After
|
||
// CWE-407 fix: LinkedHashSet for O(1) contains() with insertion-order iteration.
|
||
private static class ImportStack extends LinkedHashSet<ConfigurationClass>
|
||
```
|
||
|
||
## Patch
|
||
|
||
Fix available: `defects/spring/patch/spring-0001-0002-beanfactory-linkedhashset.patch`
|
||
|
||
Two-file patch: `BeanFactoryUtils.java` + `ConfigurationClassParser.java`.
|
||
|
||
Unit test: 6/6 pass. spring-0001 at B=200: defective=20,000, fixed=200, **200× speedup**. spring-0002: defective average 100 probes/call, fixed average 1 probe/call.
|
||
|
||
## What We Ask
|
||
|
||
A patch is ready for review.
|
||
|
||
1. Confirm receipt and assign a GitHub Security Advisory or JIRA reference.
|
||
2. Assess severity — spring-0001 fires on every hierarchical application context bean resolution.
|
||
3. Coordinate a disclosure date — we are targeting 90 days from first contact.
|
||
4. We will credit the Spring team in the public disclosure. Preferred acknowledgment format welcome.
|
||
|
||
Contact: see cover email. This brief is confidential until coordinated disclosure.
|