4.6 KiB
4.6 KiB
UNDF: UNDF-2026-000000541
spring-0006: VersionResourceResolver — O(N²) patternsList.contains() in addFixedVersionStrategy()
CWE-407 — Algorithmic Complexity: Linear Membership Test in Loop
| Field | Value |
|---|---|
| ID | spring-0006 |
| Severity | MEDIUM |
| Ecosystem | spring-framework |
| Package | spring-webmvc |
| File | spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java |
| Lines | 130–141 |
| Complexity | O(N²) where N = pathPatterns.length |
| Fix | Convert patternsList to HashSet<String> for O(1) membership check |
Defective Code
// Line 130-141: VersionResourceResolver.addFixedVersionStrategy()
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
List<String> patternsList = Arrays.asList(pathPatterns); // O(N) list
List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length);
String versionPrefix = "/" + version;
for (String pattern : patternsList) { // outer loop: O(N)
prefixedPatterns.add(pattern);
if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
// ^^^^^^^^^^^^^^^^^^^^ O(N) scan — CWE-407
prefixedPatterns.add(versionPrefix + pattern);
}
}
return addVersionStrategy(new FixedVersionStrategy(version), StringUtils.toStringArray(prefixedPatterns));
}
Pattern: for (x : list) { list.contains(y) } — inner contains() is O(N) on
Arrays.asList() backed array. Outer loop is O(N). Total: O(N²).
Triggered at application startup / configuration time when configuring versioned resource resolvers (common in Spring MVC static resource handling). Large Spring apps with many path patterns suffer quadratic cost.
Complexity Table
| N (pathPatterns) | Operations (before) | Operations (after) |
|---|---|---|
| 10 | ~100 | ~10 |
| 100 | ~10,000 | ~100 |
| 1,000 | ~1,000,000 | ~1,000 |
Speedup ratio: ~N× — 100x at N=100, 1000x at N=1000.
Fix
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
// spring-0006 fix: HashSet for O(1) membership check.
// Previously Arrays.asList() returned a plain List — patternsList.contains() was O(N).
// With N patterns, the loop called contains() N times = O(N²) total.
Set<String> patternsSet = new HashSet<>(Arrays.asList(pathPatterns));
List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length * 2);
String versionPrefix = "/" + version;
for (String pattern : pathPatterns) {
prefixedPatterns.add(pattern);
if (!pattern.startsWith(versionPrefix) && !patternsSet.contains(versionPrefix + pattern)) {
prefixedPatterns.add(versionPrefix + pattern);
}
}
return addVersionStrategy(new FixedVersionStrategy(version), StringUtils.toStringArray(prefixedPatterns));
}
Patch
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@@ -130,10 +131,13 @@ public class VersionResourceResolver extends AbstractResourceResolver {
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
- List<String> patternsList = Arrays.asList(pathPatterns);
- List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length);
+ // spring-0006 fix: use HashSet for O(1) duplicate detection; was O(N²) with List.contains()
+ Set<String> patternsSet = new HashSet<>(Arrays.asList(pathPatterns));
+ List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length * 2);
String versionPrefix = "/" + version;
- for (String pattern : patternsList) {
+ for (String pattern : pathPatterns) {
prefixedPatterns.add(pattern);
- if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
+ if (!pattern.startsWith(versionPrefix) && !patternsSet.contains(versionPrefix + pattern)) {
prefixedPatterns.add(versionPrefix + pattern);
}
}