5.1 KiB
UNDF: UNDF-2026-000000561
undertow-0001 — CWE-407: DefaultContainerConfigurator.getNegotiatedSubprotocol() List.contains() O(R×S) per WebSocket handshake
File: websockets-jsr/src/main/java/io/undertow/websockets/jsr/DefaultContainerConfigurator.java
Method: getNegotiatedSubprotocol(List<String> supported, List<String> requested)
Severity: MEDIUM
CWE: CWE-407 (Inefficient Algorithmic Complexity)
Defect
getNegotiatedSubprotocol() iterates over the client-provided requested subprotocol
list and calls supported.contains(proto) on each iteration. supported is a
List<String> (from ServerEndpointConfig.getSubprotocols() which returns
List<String> per the Jakarta WebSocket spec). List.contains() is O(S) — it
performs a linear scan via String.equals(). The outer loop runs |R| times,
giving total complexity O(|R| × |S|).
// DEFECTIVE — websockets-jsr/.../DefaultContainerConfigurator.java:50-57
@Override
public String getNegotiatedSubprotocol(final List<String> supported, final List<String> requested) {
for(String proto : requested) {
if(supported.contains(proto)) { // O(S) scan per iteration → O(R×S) total
return proto;
}
}
return "";
}
A hostile client can send |R| = 100+ subprotocol values in the
Sec-WebSocket-Protocol header. With S server-configured protocols this
becomes O(R × S) string comparisons per WebSocket upgrade request, executed
on the I/O thread.
The companion method getNegotiatedExtensions() has the same pattern:
// DEFECTIVE — nested O(|requested| × |installed|) loop
for (Extension req : requested) {
for (Extension extension : installed) {
if (extension.getName().equals(req.getName())) { ...
Fix
Convert supported to a HashSet<String> once before the loop, replacing
O(S) per-call with O(1). For extensions, build a Map<String, Extension> on
installed keyed by name.
// FIXED
@Override
public String getNegotiatedSubprotocol(final List<String> supported, final List<String> requested) {
// Build O(1)-lookup set from server-side list once, not O(S) per iteration.
Set<String> supportedSet = new HashSet<>(supported);
for (String proto : requested) {
if (supportedSet.contains(proto)) {
return proto;
}
}
return "";
}
@Override
public List<Extension> getNegotiatedExtensions(final List<Extension> installed, final List<Extension> requested) {
// Build O(1)-lookup map from installed extensions keyed by name.
Map<String, Extension> installedMap = new HashMap<>(installed.size() * 2);
for (Extension ext : installed) {
installedMap.put(ext.getName(), ext);
}
final List<Extension> ret = new ArrayList<>();
for (Extension req : requested) {
if (installedMap.containsKey(req.getName())) {
ret.add(req);
}
}
return ret;
}
Imports to add: java.util.HashMap, java.util.HashSet, java.util.Map, java.util.Set
Complexity
| Before | After | |
|---|---|---|
| getNegotiatedSubprotocol() | O(|R| × |S|) | O(|R| + |S|) |
| getNegotiatedExtensions() | O(|req| × |inst|) | O(|req| + |inst|) |
Impact
Called on every WebSocket upgrade handshake on the I/O thread. A client sending 100 requested subprotocols against a server with 50 configured subprotocols produces 5000 string comparisons. Fix reduces to 150.
Patch
--- a/websockets-jsr/src/main/java/io/undertow/websockets/jsr/DefaultContainerConfigurator.java
+++ b/websockets-jsr/src/main/java/io/undertow/websockets/jsr/DefaultContainerConfigurator.java
@@ -22,6 +22,9 @@ import io.undertow.servlet.api.InstanceHandle;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import jakarta.websocket.Extension;
@@ -50,17 +53,21 @@ public class DefaultContainerConfigurator extends ServerEndpointConfig.Configura
@Override
public String getNegotiatedSubprotocol(final List<String> supported, final List<String> requested) {
- for(String proto : requested) {
- if(supported.contains(proto)) {
+ Set<String> supportedSet = new HashSet<>(supported);
+ for (String proto : requested) {
+ if (supportedSet.contains(proto)) {
return proto;
}
}
return "";
}
@Override
public List<Extension> getNegotiatedExtensions(final List<Extension> installed, final List<Extension> requested) {
+ Map<String, Extension> installedMap = new HashMap<>(installed.size() * 2);
+ for (Extension ext : installed) {
+ installedMap.put(ext.getName(), ext);
+ }
final List<Extension> ret = new ArrayList<>();
for (Extension req : requested) {
- for (Extension extension : installed) {
- if (extension.getName().equals(req.getName())) {
- ret.add(req);
- break;
- }
+ if (installedMap.containsKey(req.getName())) {
+ ret.add(req);
}
}
return ret;
}