package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Unit test for CWE-407 undertow-0001: * DefaultContainerConfigurator.getNegotiatedSubprotocol() uses List.contains() * in a per-request loop — O(|requested| × |supported|) per WebSocket handshake. * * Real code (websockets-jsr/.../DefaultContainerConfigurator.java): * for (String proto : requested) { * if (supported.contains(proto)) { // O(S) List scan → O(R×S) total * return proto; * } * } * * Fix: convert supported to HashSet once before the loop → O(R+S). * * Also covers getNegotiatedExtensions() nested-loop O(|req|×|inst|) → O(|req|+|inst|). * * Run: javac -d . UndertowWebSocketSubprotocolTest.java && java -ea unit.UndertowWebSocketSubprotocolTest */ public class UndertowWebSocketSubprotocolTest { // ----------------------------------------------------------------------- // Slow path: List.contains() inside loop (O(R×S)) // ----------------------------------------------------------------------- static long[] slowNegotiate(List supported, List requested) { long ops = 0; String result = ""; for (String proto : requested) { // Simulate List.contains(): linear scan for (String s : supported) { ops++; if (s.equals(proto)) { result = proto; break; } } if (!result.isEmpty()) break; } return new long[]{ops, result.isEmpty() ? -1 : requested.indexOf(result)}; } // Worst case: no match, all R×S comparisons static long slowNegotiateNoMatch(List supported, List requested) { long ops = 0; for (String proto : requested) { for (String s : supported) { ops++; if (s.equals(proto)) break; } } return ops; } // ----------------------------------------------------------------------- // Fast path: HashSet.contains() (O(R+S)) // ----------------------------------------------------------------------- static long fastNegotiateOps(List supported, List requested) { // Build set once long ops = supported.size(); // cost to build HashSet Set supportedSet = new HashSet<>(supported); for (String proto : requested) { ops++; // O(1) HashSet lookup if (supportedSet.contains(proto)) break; } return ops; } // ----------------------------------------------------------------------- // Slow extension negotiation: O(|req|×|inst|) nested loop // ----------------------------------------------------------------------- static long slowExtensionNegotiateOps(List installed, List requested) { long ops = 0; for (String req : requested) { for (String inst : installed) { ops++; if (inst.equals(req)) break; } } return ops; } // Fast extension: O(|req|+|inst|) HashMap static long fastExtensionNegotiateOps(List installed, List requested) { long ops = installed.size(); // build map Map instMap = new HashMap<>(); for (String inst : installed) instMap.put(inst, true); for (String req : requested) { ops++; // O(1) map lookup } return ops; } // ----------------------------------------------------------------------- // Benchmarks // ----------------------------------------------------------------------- static long timeSlow(int R, int S, int repeats) { List supported = new ArrayList<>(); List requested = new ArrayList<>(); for (int i = 0; i < S; i++) supported.add("proto-supported-" + i); for (int i = 0; i < R; i++) requested.add("proto-requested-" + i); // no match // warmup for (int r = 0; r < 5; r++) slowNegotiateNoMatch(supported, requested); long t0 = System.nanoTime(); for (int r = 0; r < repeats; r++) slowNegotiateNoMatch(supported, requested); return (System.nanoTime() - t0) / 1_000_000; } static long timeFast(int R, int S, int repeats) { List supported = new ArrayList<>(); List requested = new ArrayList<>(); for (int i = 0; i < S; i++) supported.add("proto-supported-" + i); for (int i = 0; i < R; i++) requested.add("proto-requested-" + i); // warmup for (int r = 0; r < 5; r++) fastNegotiateOps(supported, requested); long t0 = System.nanoTime(); for (int r = 0; r < repeats; r++) fastNegotiateOps(supported, requested); return (System.nanoTime() - t0) / 1_000_000; } // ----------------------------------------------------------------------- // Test harness // ----------------------------------------------------------------------- static int passed = 0; static int failed = 0; static void assertRatio(String label, long slowOps, long fastOps, double minRatio) { double ratio = fastOps > 0 ? (double) slowOps / fastOps : slowOps; boolean ok = ratio >= minRatio; System.out.printf(" %-60s slowOps:%,7d fastOps:%,6d ratio:%.1fx %s%n", label, slowOps, fastOps, ratio, ok ? "PASS" : "FAIL"); if (ok) passed++; else failed++; } static void assertTimeRatio(String label, long slowMs, long fastMs, double minRatio) { double ratio = fastMs > 0 ? (double) slowMs / fastMs : (slowMs > 0 ? 100.0 : 1.0); boolean ok = ratio >= minRatio; System.out.printf(" %-60s slow:%4dms fast:%4dms ratio:%.1fx %s%n", label, slowMs, fastMs, ratio, ok ? "PASS" : "FAIL"); if (ok) passed++; else failed++; } static void assertCorrect(String label, List supported, List requested, String expected) { // slow Set supportedSet = new HashSet<>(supported); String slow = ""; for (String proto : requested) { if (supported.contains(proto)) { slow = proto; break; } } // fast String fast = ""; for (String proto : requested) { if (supportedSet.contains(proto)) { fast = proto; break; } } boolean ok = slow.equals(fast) && slow.equals(expected); System.out.printf(" %-60s slow='%s' fast='%s' expected='%s' %s%n", label, slow, fast, expected, ok ? "PASS" : "FAIL"); if (ok) passed++; else failed++; } public static void main(String[] args) { System.out.println("=== undertow-0001: WebSocket subprotocol negotiation List.contains() O(R×S) ==="); System.out.println(); // --- Op-count: subprotocol negotiation --- System.out.println("Op-count: getNegotiatedSubprotocol (no match, worst case):"); int[][] cases = {{10,10},{50,20},{100,50},{200,100}}; for (int[] rc : cases) { int R = rc[0], S = rc[1]; List supported = new ArrayList<>(); List requested = new ArrayList<>(); for (int i = 0; i < S; i++) supported.add("s" + i); for (int i = 0; i < R; i++) requested.add("r" + i); // no overlap long slowOps = slowNegotiateNoMatch(supported, requested); long fastOps = fastNegotiateOps(supported, requested); double minRatio = R >= 100 ? 5.0 : 3.0; assertRatio(String.format("negotiate no-match R=%d S=%d", R, S), slowOps, fastOps, minRatio); } System.out.println(); // --- Op-count: extension negotiation --- System.out.println("Op-count: getNegotiatedExtensions (no match, worst case):"); for (int[] rc : cases) { int R = rc[0], S = rc[1]; List installed = new ArrayList<>(); List requested = new ArrayList<>(); for (int i = 0; i < S; i++) installed.add("ext-inst-" + i); for (int i = 0; i < R; i++) requested.add("ext-req-" + i); long slowOps = slowExtensionNegotiateOps(installed, requested); long fastOps = fastExtensionNegotiateOps(installed, requested); double minRatio = R >= 100 ? 5.0 : 3.0; assertRatio(String.format("extensions no-match R=%d S=%d", R, S), slowOps, fastOps, minRatio); } System.out.println(); // --- Correctness --- System.out.println("Correctness (first match returned):"); List sup1 = List.of("chat", "binary", "json"); List req1 = List.of("xml", "json", "chat"); assertCorrect("first client-preferred match is 'json'", sup1, req1, "json"); List sup2 = List.of("v1", "v2", "v3"); List req2 = List.of("v4", "v5"); assertCorrect("no match returns ''", sup2, req2, ""); List sup3 = List.of("proto"); List req3 = List.of("proto"); assertCorrect("exact single match", sup3, req3, "proto"); System.out.println(); // --- Wall-clock --- System.out.println("Wall-clock timing (R=1000 S=500, 2000 repeats):"); long slowMs = timeSlow(1000, 500, 2000); long fastMs = timeFast(1000, 500, 2000); assertTimeRatio("negotiate R=1000 S=500 x2000", slowMs, fastMs, 5.0); System.out.println(); System.out.printf("Result: %d/%d PASS%n", passed, passed + failed); if (failed > 0) System.exit(1); } }