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,251 @@
package unit;
import java.util.*;
/**
* SpringFrameworkTest CWE-407 benchmark for spring-framework defects.
*
* spring-framework-0001: CorsConfiguration.checkHeaders() O(R×A)
* checkHeaders() does a nested loop: for each of R request headers,
* it scans A allowedHeaders via equalsIgnoreCase(). Total O(R×A).
* Fix: build a case-insensitive Set<String> once O(A), then O(1) lookup
* per request header O(R+A) total.
*
* spring-framework-0002: AcceptHeaderLocaleResolver.findSupportedLocale() O(R×S)
* For each of R Accept-Language locales, calls supportedLocales.contains(locale)
* on an ArrayList O(S) linear scan. Total O(R×S) per request.
* Same defect in AcceptHeaderLocaleContextResolver (reactive).
* Fix: use LinkedHashSet for O(1) full-locale match O(R+S) total.
*
* spring-framework-0003: EncodedResourceResolver.contentCodings ArrayList O(A×C)
* resolveResourceInternal() iterates acceptedCodings from the request header,
* calling this.contentCodings.contains(acceptedCoding) on an ArrayList O(C)
* per accepted coding. Total O(A×C) per static resource request.
* Fix: change contentCodings to LinkedHashSet O(1) contains O(A) total.
*/
public class SpringFrameworkTest {
// -----------------------------------------------------------------------
// spring-framework-0001: CorsConfiguration.checkHeaders O(R×A)
// -----------------------------------------------------------------------
/** Unpatched: inner for-loop scan of allowedHeaders for each request header. */
static long checkHeadersUnpatched(int numRequestHeaders, int numAllowedHeaders) {
List<String> requestHeaders = new ArrayList<>(numRequestHeaders);
for (int i = 0; i < numRequestHeaders; i++) requestHeaders.add("x-custom-req-" + i);
List<String> allowedHeaders = new ArrayList<>(numAllowedHeaders);
for (int i = 0; i < numAllowedHeaders; i++) allowedHeaders.add("x-allowed-" + i);
long ops = 0;
List<String> result = new ArrayList<>();
for (String requestHeader : requestHeaders) {
for (String allowedHeader : allowedHeaders) {
ops++; // equalsIgnoreCase probe
if (requestHeader.equalsIgnoreCase(allowedHeader)) {
result.add(requestHeader);
break;
}
}
}
return ops;
}
/** Patched: build lowercase Set once, O(1) lookup per request header. */
static long checkHeadersPatched(int numRequestHeaders, int numAllowedHeaders) {
List<String> requestHeaders = new ArrayList<>(numRequestHeaders);
for (int i = 0; i < numRequestHeaders; i++) requestHeaders.add("x-custom-req-" + i);
List<String> allowedHeaders = new ArrayList<>(numAllowedHeaders);
for (int i = 0; i < numAllowedHeaders; i++) allowedHeaders.add("x-allowed-" + i);
long ops = 0;
Set<String> allowedSet = new LinkedHashSet<>(numAllowedHeaders * 2);
for (String h : allowedHeaders) {
allowedSet.add(h.toLowerCase(Locale.ROOT));
ops++; // set build: O(A)
}
List<String> result = new ArrayList<>();
for (String requestHeader : requestHeaders) {
ops++; // O(1) hash lookup
if (allowedSet.contains(requestHeader.toLowerCase(Locale.ROOT))) {
result.add(requestHeader);
}
}
return ops;
}
// -----------------------------------------------------------------------
// spring-framework-0002: AcceptHeaderLocaleResolver O(R×S)
// -----------------------------------------------------------------------
/**
* Unpatched: ArrayList.contains() = O(S) linear scan per request locale.
* Tests the primary hot path: full-locale exact match via contains().
* Uses country-code locales so there is no language-only fallback hit.
*/
static long resolveLocaleUnpatched(int numRequestLocales, int numSupportedLocales) {
// Request locales: zh-XX0, zh-XX1, ... (no match with supported set)
List<Locale> requestLocales = new ArrayList<>(numRequestLocales);
for (int i = 0; i < numRequestLocales; i++) {
requestLocales.add(new Locale("zh", "XX" + i));
}
// Supported locales: en-ZZ0, en-ZZ1, ... (different language, no overlap)
List<Locale> supportedLocales = new ArrayList<>(numSupportedLocales);
for (int i = 0; i < numSupportedLocales; i++) {
supportedLocales.add(new Locale("en", "ZZ" + i));
}
long ops = 0;
for (Locale locale : requestLocales) {
// Simulates ArrayList.contains(locale) O(S) scan (no early exit, no match)
for (Locale s : supportedLocales) {
ops++;
if (s.equals(locale)) break;
}
}
return ops;
}
/**
* Patched: LinkedHashSet.contains() = O(1) per request locale.
* Build the set once O(S), then O(1) per request locale.
*/
static long resolveLocalePatched(int numRequestLocales, int numSupportedLocales) {
List<Locale> requestLocales = new ArrayList<>(numRequestLocales);
for (int i = 0; i < numRequestLocales; i++) {
requestLocales.add(new Locale("zh", "XX" + i));
}
List<Locale> supportedLocalesList = new ArrayList<>(numSupportedLocales);
for (int i = 0; i < numSupportedLocales; i++) {
supportedLocalesList.add(new Locale("en", "ZZ" + i));
}
long ops = 0;
// Build set once O(S)
Set<Locale> supportedSet = new LinkedHashSet<>(supportedLocalesList);
ops += numSupportedLocales;
for (Locale locale : requestLocales) {
ops++; // O(1) set lookup
boolean found = supportedSet.contains(locale);
}
return ops;
}
// -----------------------------------------------------------------------
// spring-framework-0003: EncodedResourceResolver.contentCodings O(A×C)
// -----------------------------------------------------------------------
/** Unpatched: ArrayList.contains() = O(C) scan per accepted-encoding token. */
static long encodedResolverUnpatched(int numAcceptedCodings, int numContentCodings) {
List<String> acceptedCodings = new ArrayList<>(numAcceptedCodings);
for (int i = 0; i < numAcceptedCodings; i++) acceptedCodings.add("enc-acc-" + i);
List<String> contentCodings = new ArrayList<>(numContentCodings);
for (int i = 0; i < numContentCodings; i++) contentCodings.add("enc-cfg-" + i);
long ops = 0;
for (String accepted : acceptedCodings) {
// Simulates ArrayList.contains(accepted) O(C) scan
for (String supported : contentCodings) {
ops++;
if (supported.equals(accepted)) break;
}
}
return ops;
}
/** Patched: LinkedHashSet.contains() = O(1) per accepted-encoding token. */
static long encodedResolverPatched(int numAcceptedCodings, int numContentCodings) {
List<String> acceptedCodings = new ArrayList<>(numAcceptedCodings);
for (int i = 0; i < numAcceptedCodings; i++) acceptedCodings.add("enc-acc-" + i);
Set<String> contentCodings = new LinkedHashSet<>(numContentCodings * 2);
for (int i = 0; i < numContentCodings; i++) contentCodings.add("enc-cfg-" + i);
long ops = 0;
for (String accepted : acceptedCodings) {
ops++; // O(1) hash lookup
boolean found = contentCodings.contains(accepted);
}
return ops;
}
// -----------------------------------------------------------------------
// main
// -----------------------------------------------------------------------
public static void main(String[] args) {
int pass = 0, total = 0;
// spring-framework-0001: R=20, A=50 worst case no match (all custom headers differ)
{
total++;
int R = 20, A = 50;
long slow = checkHeadersUnpatched(R, A);
long fast = checkHeadersPatched(R, A);
double ratio = (double) slow / Math.max(fast, 1);
boolean ok = ratio > 10.0 && slow >= (long) R * A;
if (ok) pass++;
System.out.printf(" %s spring-framework-0001: checkHeaders ArrayList O(R×A) → Set O(R+A)"
+ " R=%d A=%d slow=%,d fast=%,d ratio=%.0fx%n",
ok ? "PASS" : "FAIL", R, A, slow, fast, ratio);
if (!ok) {
System.err.printf(" FAIL spring-framework-0001: expected ratio>10x and slow>=%d, got ratio=%.1f slow=%d%n",
(long) R * A, ratio, slow);
}
}
// spring-framework-0002: R=15, S=20 worst case no match
{
total++;
int R = 15, S = 20;
long slow = resolveLocaleUnpatched(R, S);
long fast = resolveLocalePatched(R, S);
double ratio = (double) slow / Math.max(fast, 1);
// Unpatched worst case: R×S full scans; patched: S+R ops
boolean ok = ratio > 5.0 && slow >= (long) R * S;
if (ok) pass++;
System.out.printf(" %s spring-framework-0002: localeResolver ArrayList O(R×S) → Set O(R+S)"
+ " R=%d S=%d slow=%,d fast=%,d ratio=%.0fx%n",
ok ? "PASS" : "FAIL", R, S, slow, fast, ratio);
if (!ok) {
System.err.printf(" FAIL spring-framework-0002: expected ratio>5x and slow>=%d, got ratio=%.1f slow=%d%n",
(long) R * S, ratio, slow);
}
}
// spring-framework-0003: A=10 accepted codings (exaggerated), C=8 configured codings
{
total++;
int A = 10, C = 8;
long slow = encodedResolverUnpatched(A, C);
long fast = encodedResolverPatched(A, C);
double ratio = (double) slow / Math.max(fast, 1);
// Unpatched worst case: A×C; patched: A ops
boolean ok = ratio > 3.0 && slow >= (long) A * C;
if (ok) pass++;
System.out.printf(" %s spring-framework-0003: encodedResolver ArrayList O(A×C) → Set O(A)"
+ " A=%d C=%d slow=%,d fast=%,d ratio=%.0fx%n",
ok ? "PASS" : "FAIL", A, C, slow, fast, ratio);
if (!ok) {
System.err.printf(" FAIL spring-framework-0003: expected ratio>3x and slow>=%d, got ratio=%.1f slow=%d%n",
(long) A * C, ratio, slow);
}
}
System.out.println();
System.out.printf("%d/%d PASS%n", pass, total);
if (pass < total) {
System.out.println("FAIL");
System.exit(1);
}
System.out.println("ALL PASS");
}
}