4.5 KiB
UNDF: UNDF-2026-000000458
mbedtls-0001: CWE-407 O(S×C) ALPN selection in mbedtls_ssl_parse_alpn_ext
Severity: MEDIUM
Location
library/ssl_tls.c — mbedtls_ssl_parse_alpn_ext()
Description
mbedtls_ssl_parse_alpn_ext() selects a common ALPN protocol from the client's
ClientHello extension. The server has a NULL-terminated list of S configured
protocol names (ssl->conf->alpn_list); the client sends a length-prefixed
list of C protocol names.
The current implementation:
/* outer: iterate every server-configured ALPN name (S entries) */
for (const char *const *alpn = ssl->conf->alpn_list; *alpn != NULL; alpn++) {
size_t const alpn_len = strlen(*alpn);
p = protocol_name_list;
/* inner: scan entire client list for each server entry */
while (p < protocol_name_list_end) {
protocol_name_len = *p++;
if (protocol_name_len == alpn_len &&
memcmp(p, *alpn, alpn_len) == 0) { /* O(L) per call */
ssl->alpn_chosen = *alpn;
return 0;
}
p += protocol_name_len;
}
}
Total work: O(S × C × L) where L = average protocol name length.
An attacker can send a ClientHello with C distinct ALPN names (the extension length field allows up to 65535 bytes; at a minimum name length of 1 byte each that is ~32767 names). Each triggers a full scan of the server's ALPN list.
Complexity Before Fix
O(S × C × L) per handshake.
Fix
Build a hash map of the client's ALPN list once (O(C×L)), then look up each server-preferred entry in O(L) expected. Total: O((C + S) × L).
--- a/library/ssl_tls.c
+++ b/library/ssl_tls.c
@@ -8401,6 +8401,10 @@ int mbedtls_ssl_parse_alpn_ext(mbedtls_ssl_context *ssl,
{
const unsigned char *p = buf;
size_t protocol_name_list_len;
+ /* Hash set: store (ptr, len) of each client name; 64-slot open-addressing */
+#define ALPN_HS 64
+ const unsigned char *hs_ptr[ALPN_HS];
+ uint8_t hs_len[ALPN_HS];
const unsigned char *protocol_name_list;
const unsigned char *protocol_name_list_end;
size_t protocol_name_len;
@@ -8441,15 +8445,30 @@ int mbedtls_ssl_parse_alpn_ext(mbedtls_ssl_context *ssl,
p += protocol_name_len;
}
- /* Use our order of preference */
- for (const char *const *alpn = ssl->conf->alpn_list; *alpn != NULL; alpn++) {
- size_t const alpn_len = strlen(*alpn);
- p = protocol_name_list;
- while (p < protocol_name_list_end) {
- protocol_name_len = *p++;
- if (protocol_name_len == alpn_len &&
- memcmp(p, *alpn, alpn_len) == 0) {
- ssl->alpn_chosen = *alpn;
- return 0;
- }
- p += protocol_name_len;
+ /* Build hash set of client-offered names (key = content, len pair) */
+ memset(hs_ptr, 0, sizeof(hs_ptr));
+ memset(hs_len, 0, sizeof(hs_len));
+ p = protocol_name_list;
+ while (p < protocol_name_list_end) {
+ protocol_name_len = *p++;
+ /* FNV-1a hash of content bytes */
+ uint32_t h = 2166136261u;
+ for (size_t k = 0; k < protocol_name_len; k++)
+ h = (h ^ p[k]) * 16777619u;
+ uint32_t slot = h & (ALPN_HS - 1);
+ while (hs_ptr[slot] != NULL &&
+ !(hs_len[slot] == protocol_name_len &&
+ memcmp(hs_ptr[slot], p, protocol_name_len) == 0))
+ slot = (slot + 1) & (ALPN_HS - 1);
+ hs_ptr[slot] = p;
+ hs_len[slot] = (uint8_t) protocol_name_len;
+ p += protocol_name_len;
+ }
+
+ /* Use our order of preference — now O(S) with O(1) lookup per entry */
+ for (const char *const *alpn = ssl->conf->alpn_list; *alpn != NULL; alpn++) {
+ size_t const alpn_len = strlen(*alpn);
+ uint32_t h = 2166136261u;
+ for (size_t k = 0; k < alpn_len; k++)
+ h = (h ^ (unsigned char)(*alpn)[k]) * 16777619u;
+ uint32_t slot = h & (ALPN_HS - 1);
+ while (hs_ptr[slot] != NULL) {
+ if (hs_len[slot] == alpn_len &&
+ memcmp(hs_ptr[slot], *alpn, alpn_len) == 0) {
+ ssl->alpn_chosen = *alpn;
+ return 0;
+ }
+ slot = (slot + 1) & (ALPN_HS - 1);
}
}
+#undef ALPN_HS
Overhead Removed
With S=10 server ALPN names and C=100 client ALPN names: ~1000 memcmp calls → ~110 (10× speedup). With C=1000: ~10000 → ~1010 (10× speedup, grows unbounded with client-controlled C).
References
- RFC 7301 §3.1 — ALPN extension format (client list is variable-length)
- CWE-407: Inefficient Algorithmic Complexity