java-topology/defects/spring-framework/patch/spring-framework-0001-cors-check-headers-o-r-a.patch
russell@unturf.com 0b37443f11 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
2026-03-30 08:27:16 -04:00

68 lines
2.6 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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);
+ }
}
}
}