110 lines
4.8 KiB
Diff
110 lines
4.8 KiB
Diff
# UNDF: UNDF-2026-000000295
|
||
From 5708b73 Mon Sep 17 00:00:00 2001
|
||
Subject: [PATCH] CWE-407: spring-0001/0002 — fix O(B²) contains() in
|
||
mergeNamesWithParent and ImportStack
|
||
|
||
spring-0001 (HIGH): BeanFactoryUtils.mergeNamesWithParent() used an
|
||
ArrayList for duplicate-checking, making merged.contains(beanName) an
|
||
O(|result|) scan for every element of parentResult — total O(|result| ×
|
||
|parentResult|) = O(B²) where B is total bean count. Every hierarchical
|
||
ApplicationContext lookup (beanNamesForType, beanNamesForAnnotation, etc.)
|
||
hits this path; Spring Boot apps with large contexts pay the tax on every
|
||
container refresh.
|
||
|
||
Fix: replace ArrayList with LinkedHashSet (insertion-ordered, O(1)
|
||
contains/add), call merged.add(beanName) directly and skip the redundant
|
||
contains() guard — Set.add() is idempotent. Convert back to String[] via
|
||
toArray() at the end.
|
||
|
||
spring-0002 (LOW-MEDIUM): ImportStack extends ArrayDeque<ConfigurationClass>.
|
||
ArrayDeque.contains() is O(n). It is called in processMemberClasses()
|
||
(~line 422) and isChainedImportOnStack() (~line 653) — once per nested
|
||
class / import candidate. Fix: add a parallel HashSet<ConfigurationClass>
|
||
field; override push/pop/clear to maintain it; override contains() to
|
||
delegate to the set, giving O(1) membership tests.
|
||
|
||
CWE: CWE-407 (Inefficient Algorithmic Complexity)
|
||
Severity: spring-0001 HIGH, spring-0002 LOW-MEDIUM
|
||
---
|
||
.../beans/factory/BeanFactoryUtils.java | 18 ++++++------
|
||
.../annotation/ConfigurationClassParser.java | 28 ++++++++++++++++---
|
||
2 files changed, 33 insertions(+), 13 deletions(-)
|
||
|
||
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
|
||
index aaaaaaa..bbbbbbb 100644
|
||
--- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
|
||
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java
|
||
@@ -19,6 +19,7 @@ import java.lang.annotation.Annotation;
|
||
import java.util.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.LinkedHashMap;
|
||
+import java.util.LinkedHashSet;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.concurrent.ConcurrentHashMap;
|
||
@@ -521,13 +521,13 @@ public abstract class BeanFactoryUtils {
|
||
* @since 4.3.15
|
||
*/
|
||
private static String[] mergeNamesWithParent(String[] result, String[] parentResult, HierarchicalBeanFactory hbf) {
|
||
if (parentResult.length == 0) {
|
||
return result;
|
||
}
|
||
- List<String> merged = new ArrayList<>(result.length + parentResult.length);
|
||
- merged.addAll(Arrays.asList(result));
|
||
+ // CWE-407 fix (spring-0001): use LinkedHashSet for O(1) contains/add instead
|
||
+ // of ArrayList which makes merged.contains() O(|result|) per iteration —
|
||
+ // total O(|result| × |parentResult|) = O(B²) over all beans.
|
||
+ LinkedHashSet<String> merged = new LinkedHashSet<>(Arrays.asList(result));
|
||
for (String beanName : parentResult) {
|
||
- if (!merged.contains(beanName) && !hbf.containsLocalBean(beanName)) {
|
||
- merged.add(beanName);
|
||
+ if (!hbf.containsLocalBean(beanName)) {
|
||
+ merged.add(beanName); // Set.add() is idempotent; no contains() needed
|
||
}
|
||
}
|
||
- return StringUtils.toStringArray(merged);
|
||
+ return merged.toArray(String[]::new);
|
||
}
|
||
|
||
diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
|
||
index ccccccc..ddddddd 100644
|
||
--- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
|
||
+++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
|
||
@@ -752,7 +752,31 @@ class ConfigurationClassParser {
|
||
@SuppressWarnings("serial")
|
||
private class ImportStack extends ArrayDeque<ConfigurationClass> implements ImportRegistry {
|
||
|
||
+ // CWE-407 fix (spring-0002): ArrayDeque.contains() is O(n). ImportStack is
|
||
+ // queried in processMemberClasses() and isChainedImportOnStack() — once per
|
||
+ // nested class / import candidate. A parallel HashSet gives O(1) membership.
|
||
+ private final HashSet<ConfigurationClass> members = new HashSet<>();
|
||
+
|
||
private final MultiValueMap<String, AnnotationMetadata> imports = new LinkedMultiValueMap<>();
|
||
|
||
+ @Override
|
||
+ public void push(ConfigurationClass item) {
|
||
+ super.push(item);
|
||
+ this.members.add(item);
|
||
+ }
|
||
+
|
||
+ @Override
|
||
+ public ConfigurationClass pop() {
|
||
+ ConfigurationClass item = super.pop();
|
||
+ this.members.remove(item);
|
||
+ return item;
|
||
+ }
|
||
+
|
||
+ @Override
|
||
+ public void clear() {
|
||
+ super.clear();
|
||
+ this.members.clear();
|
||
+ }
|
||
+
|
||
+ @Override
|
||
+ public boolean contains(Object o) {
|
||
+ return this.members.contains(o);
|
||
+ }
|
||
+
|
||
void registerImport(AnnotationMetadata importingClass, String importedClass) {
|
||
this.imports.add(importedClass, importingClass);
|
||
}
|