java-topology/defects/spring-framework/unit/SpringFrameworkTest.java
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

251 lines
11 KiB
Java
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.

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");
}
}