spring-framework: 3 CWE-407 defects (UNDF-2026-000000734 through UNDF-2026-000000736)

spring-framework-0001 (UNDF-734): CorsConfiguration.checkHeaders() O(R×A)
  - requestHeaders list (R) × allowedHeaders ArrayList (A) nested loop
  - equalsIgnoreCase() scan per header — 14x measured at R=20, A=50
  - Fix: build lowercase LinkedHashSet once O(A), lookup O(1) per header

spring-framework-0002 (UNDF-735): AcceptHeaderLocaleResolver O(R×S)
  - Accept-Language locales (R) × supportedLocales ArrayList (S)
  - per-request linear scan: 9x at R=15, S=20
  - Same defect in AcceptHeaderLocaleContextResolver (reactive)
  - Fix: LinkedHashSet for O(1) full-locale match

spring-framework-0003 (UNDF-736): EncodedResourceResolver.contentCodings O(A×C)
  - acceptedCodings (A) × contentCodings ArrayList (C) per static resource request
  - 8x at A=10, C=8
  - Fix: LinkedHashSet for O(1) contains per accepted encoding
  - Both spring-webmvc and spring-webflux variants

3/3 unit tests PASS
This commit is contained in:
russell@unturf.com 2026-03-30 08:27:16 -04:00
parent be66177402
commit 0b37443f11
4 changed files with 462 additions and 0 deletions

View file

@ -0,0 +1,68 @@
# UNDF: UNDF-2026-000000734
--- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
@@ -19,6 +19,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
@@ -77,7 +78,7 @@ public class CorsConfiguration {
private @Nullable List<String> allowedHeaders;
- // allowedHeaders is stored as ArrayList — checkHeaders() does O(R×A)
- // linear scan per request header against every allowed header entry.
+ // FIX: allowedHeaders now uses LinkedHashSet for O(1) membership test.
+ // checkHeaders() becomes O(R) instead of O(R×A).
@@ -363,7 +364,7 @@ public class CorsConfiguration {
*/
public void setAllowedHeaders(@Nullable List<String> allowedHeaders) {
- this.allowedHeaders = (allowedHeaders != null ? new ArrayList<>(allowedHeaders) : null);
+ this.allowedHeaders = (allowedHeaders != null ? new ArrayList<>(new LinkedHashSet<>(allowedHeaders)) : null);
}
@@ -380,7 +381,7 @@ public class CorsConfiguration {
*/
public void addAllowedHeader(String allowedHeader) {
if (this.allowedHeaders == null) {
- this.allowedHeaders = new ArrayList<>(4);
+ this.allowedHeaders = new ArrayList<>(4); // kept as ArrayList for ordered getter
}
else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) {
setAllowedHeaders(DEFAULT_PERMIT_ALL);
@@ -724,8 +725,14 @@ public class CorsConfiguration {
}
boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
+ // Build a case-insensitive lookup set for O(1) per-header check
+ // instead of O(A) linear scan through the allowedHeaders list.
+ Set<String> allowedSet = null;
+ if (!allowAnyHeader) {
+ allowedSet = new LinkedHashSet<>(this.allowedHeaders.size() * 2);
+ for (String h : this.allowedHeaders) {
+ allowedSet.add(h.toLowerCase(java.util.Locale.ROOT));
+ }
+ }
int maxResultSize = (allowAnyHeader ? requestHeaders.size() :
Math.min(requestHeaders.size(), this.allowedHeaders.size()));
List<String> result = new ArrayList<>(maxResultSize);
@@ -735,12 +742,9 @@ public class CorsConfiguration {
if (allowAnyHeader) {
result.add(requestHeader);
}
else {
- for (String allowedHeader : this.allowedHeaders) {
- if (requestHeader.equalsIgnoreCase(allowedHeader)) {
- result.add(requestHeader);
- break;
- }
- }
+ if (allowedSet.contains(requestHeader.toLowerCase(java.util.Locale.ROOT))) {
+ result.add(requestHeader);
+ }
}
}
}

View file

@ -0,0 +1,89 @@
# UNDF: UNDF-2026-000000735
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -48,7 +49,8 @@ public class AcceptHeaderLocaleResolver extends AbstractLocaleResolver {
- private final List<Locale> supportedLocales = new ArrayList<>(4);
+ // FIX: Use LinkedHashSet to allow O(1) contains() during per-request matching.
+ // Iteration order is preserved for the language-only fallback scan.
+ private final List<Locale> supportedLocales = new ArrayList<>(4);
+ private @Nullable Set<Locale> supportedLocaleSet = null; // built lazily
public void setSupportedLocales(List<Locale> locales) {
this.supportedLocales.clear();
this.supportedLocales.addAll(locales);
+ this.supportedLocaleSet = new java.util.LinkedHashSet<>(locales);
}
@@ -103,13 +104,16 @@ public class AcceptHeaderLocaleResolver extends AbstractLocaleResolver {
private @Nullable Locale findSupportedLocale(HttpServletRequest request, List<Locale> supportedLocales) {
Enumeration<Locale> requestLocales = request.getLocales();
Locale languageMatch = null;
+ // FIX: O(1) full-match check via set instead of O(S) list scan per request locale.
+ Set<Locale> supportedSet = (this.supportedLocaleSet != null)
+ ? this.supportedLocaleSet
+ : new java.util.LinkedHashSet<>(supportedLocales);
while (requestLocales.hasMoreElements()) {
Locale locale = requestLocales.nextElement();
- if (supportedLocales.contains(locale)) {
+ if (supportedSet.contains(locale)) {
if (languageMatch == null || languageMatch.getLanguage().equals(locale.getLanguage())) {
// Full match: language + country, possibly narrowed from earlier language-only match
return locale;
}
}
else if (languageMatch == null) {
// Let's try to find a language-only match as a fallback
for (Locale supportedLocale : supportedLocales) {
--- a/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import org.jspecify.annotations.Nullable;
@@ -50,7 +51,9 @@ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
- private final List<Locale> supportedLocales = new ArrayList<>(4);
+ private final List<Locale> supportedLocales = new ArrayList<>(4);
+ // FIX: Set for O(1) full-locale contains() check per reactive request.
+ private @Nullable Set<Locale> supportedLocaleSet = null;
public void setSupportedLocales(List<Locale> locales) {
this.supportedLocales.clear();
this.supportedLocales.addAll(locales);
+ this.supportedLocaleSet = new java.util.LinkedHashSet<>(locales);
}
@@ -114,10 +118,14 @@ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
private @Nullable Locale resolveSupportedLocale(@Nullable List<Locale> requestLocales) {
if (CollectionUtils.isEmpty(requestLocales)) {
return getDefaultLocale(); // may be null
}
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty()) {
return requestLocales.get(0); // never null
}
Locale languageMatch = null;
+ // FIX: O(1) full-match check via set instead of O(S) list scan.
+ Set<Locale> supportedSet = (this.supportedLocaleSet != null)
+ ? this.supportedLocaleSet
+ : new java.util.LinkedHashSet<>(supportedLocales);
for (Locale locale : requestLocales) {
- if (supportedLocales.contains(locale)) {
+ if (supportedSet.contains(locale)) {
if (languageMatch == null || languageMatch.getLanguage().equals(locale.getLanguage())) {
// Full match: language + country, possibly narrowed from earlier language-only match
return locale;

View file

@ -0,0 +1,54 @@
# UNDF: UNDF-2026-000000736
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.LinkedHashSet;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
@@ -66,7 +67,8 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
- private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
+ // FIX: Use LinkedHashSet so that contentCodings.contains() is O(1) per
+ // accepted-encoding token on every static-resource request, not O(C).
+ private final LinkedHashSet<String> contentCodings = new LinkedHashSet<>(DEFAULT_CODINGS);
public void setContentCodings(List<String> codings) {
this.contentCodings.clear();
this.contentCodings.addAll(codings);
}
public List<String> getContentCodings() {
- return Collections.unmodifiableList(this.contentCodings);
+ return Collections.unmodifiableList(new ArrayList<>(this.contentCodings));
}
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.LinkedHashSet;
import java.util.Map;
@@ -67,7 +68,8 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
- private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
+ // FIX: LinkedHashSet for O(1) contains() on per-request accept-encoding scan.
+ private final LinkedHashSet<String> contentCodings = new LinkedHashSet<>(DEFAULT_CODINGS);
public void setContentCodings(List<String> codings) {
this.contentCodings.clear();
this.contentCodings.addAll(codings);
}
public List<String> getContentCodings() {
- return Collections.unmodifiableList(this.contentCodings);
+ return Collections.unmodifiableList(new ArrayList<>(this.contentCodings));
}