Engineering research / security

Netty Security Updates Close an mTLS Session Resumption Bypass

Netty 4.2.20.Final and 4.1.140.Final, released on 2 November 2026, fix ten and eight advisories. How a TLS session minted without a client certificate could be resumed on a context that requires one, what the DNS and HTTP fixes change, what a local check of the jars showed, and what to upgrade.

By Sandeep Belgavi · Published 2026-11-03

Recent development · Practical technical guide · 3 November 2026

Netty 4.2.20.Final and 4.1.140.Final, released on 2 November 2026, fix ten and eight advisories. How a TLS session minted without a client certificate could be resumed on a context that requires one, what the DNS and HTTP fixes change, what a local check of the jars showed, and what to upgrade.

What was released

On 2 November 2026 the Netty project released 4.2.20.Final and 4.1.140.Final. The 4.2 notes list ten security advisories and the 4.1 notes eight: three out-of-bounds reads in netty-buffer, unbounded memory use in the HTTP/2 and MQTT codecs, insufficient entropy in the DNS resolver, control-character neutralisation in the HTTP codec, and an mTLS authentication bypass through session resumption; 4.2 adds an HTTP/3 CPU issue and a use-after-free in the QUIC classes. When I checked on 3 November, the linked advisory pages returned HTTP 404, so this article relies on the release notes and the fix commits, whose messages describe the problems in detail.

Netty sits under many Java servers and clients, including gRPC Java, Reactor Netty and several web frameworks, so most Java services carry it whether or not they use it directly.

The mTLS bypass, step by step

TLS session resumption lets a returning client skip the full handshake by presenting a session ticket. OpenSSL ties a resumable session to a session ID context, and its documentation says each application must set its own context so sessions from one cannot be reused in another. According to the fix commit, Netty's OpenSSL-based server context set the same hard-coded context, the bytes of "netty", on every server SslContext, and the QUIC codec set none, which BoringSSL treats as the same empty value everywhere.

That only matters when two server contexts share session-ticket keys, which the commit calls a documented pattern for resumption across a cluster or across virtual hosts selected by SNI. Suppose one listener uses ClientAuth.NONE for a public site and another uses ClientAuth.REQUIRE for a partner API, and both share ticket keys. A client completes a handshake on the public listener without a certificate, receives a ticket, then presents it to the partner listener. The session context matches, the abbreviated handshake succeeds, and the client is connected to the mTLS listener without ever presenting a certificate.

What the fix changes, including for resumption

  • Each server context now derives its session ID context as a SHA-256 digest of the ClientAuth mode, the certificate chain and the trusted certificates. Identically configured contexts, such as the nodes of a cluster sharing ticket keys, still resume each other's sessions; differently configured ones no longer do.
  • If a context uses a custom TrustManagerFactory or KeyManagerFactory that cannot be digested, a random salt is mixed in, so such contexts are never treated as equivalent. Expect cross-node resumption to stop for those contexts after the upgrade, and with it some extra full handshakes.
  • As a second line of defence, a server handshake that completes under ClientAuth.REQUIRE without a client certificate now fails, in both the OpenSSL engine and QUIC.
  • QUIC servers with ClientAuth.REQUIRE never accept 0-RTT early data, because early data would reach the application before the certificate check runs.

The commit message names the OpenSSL/tcnative provider and the QUIC codec as affected. It does not mention the JDK SSLEngine provider, and I did not test it. If you use the OpenSSL provider, share ticket keys and run listeners with different client-auth settings, treat this as the priority item.

DNS: two defences, both off by default

The resolver advisory is described as insufficient entropy. The fix commit explains that Netty's DNS resolver authenticated UDP responses only by server address, source port and the 16-bit transaction ID, so an off-path attacker who can guess the port needs only to guess the ID to poison the cache. It adds the two defences RFC 5452 and RFC 7873 describe: randomising the case of letters in the query name and requiring the answer to echo it exactly (often called 0x20 encoding), and DNS cookies. Both are opt-in through DnsNameResolverBuilder.caseRandomizationEnabled and dnsCookiesEnabled, disabled by default because some name servers do not preserve case. The OCSP certificate validator's own resolver enables both.

So upgrading alone does not change your resolver's behaviour. If a service uses Netty's resolver against servers you control, test with both options on and keep them on if lookups still succeed.

HTTP request lines: what I checked locally

RFC 9112 allows only visible characters in a request target. The fix makes HttpUtil.isEncodingSafeStartLineToken reject every character up to and including space, and DEL, where it previously rejected only CR, LF and space; the commit notes that lenient back ends may treat a tab as a separator and C-string back ends may truncate at NUL. I compiled a small program against both 4.2.19.Final and 4.2.20.Final jars from Maven Central and constructed requests with JDK 23.0.2.

Request target4.2.19.Final4.2.20.Final
/orders/BK-1042acceptedaccepted
with a tab before HTTP/1.0acceptedrejected
with a NUL byteacceptedrejected
with DELacceptedrejected
with a spacerejectedrejected

The same program confirmed that the two resolver options and SslContext.deriveSessionIdContext exist only in 4.2.20. Code that builds outbound requests from user input, such as a proxy or a client that forwards paths, will now throw IllegalArgumentException where it used to send the bytes on. That is the intended change, but it surfaces as new errors in logs.

What to upgrade and how to check

  1. Find the Netty version each service actually resolves: mvn dependency:tree -Dincludes=io.netty or gradle dependencies, plus the version in the built artifact's lock or SBOM. Frameworks pin Netty, so a framework upgrade may be the path. Copies shaded into another jar under a relocated package, such as grpc-netty-shaded, do not appear under io.netty at all and are fixed only by a release of the library that shades them.
  2. Upgrade to 4.2.20.Final on the 4.2 line or 4.1.140.Final on the 4.1 line, preferably through the netty-bom so all modules move together; mixed module versions are a common source of runtime errors.
  3. If you use the OpenSSL provider, also check the tcnative and QUIC native artifacts that ship with your build.
  4. After deploying, watch TLS resumption rates on listeners with custom trust or key managers, and errors from request construction.
  5. If you share ticket keys between listeners with different client-auth settings, stop sharing them regardless of version; it was never necessary for them to resume each other.

Limits of this review

I did not reproduce the mTLS bypass, the memory issues or the out-of-bounds reads; the descriptions above come from the commit messages and release notes. The advisories may add affected ranges and severity once they are visible. Re-check them before marking the upgrade done.

Sources and scope

Checked against these sources on 3 November 2026. Event: Netty 4.2.20.Final and 4.1.140.Final released, 2 November 2026; recheck it before acting. Local check on 3 November 2026: netty 4.2.19.Final and 4.2.20.Final jars from Maven Central, JDK 23.0.2, one Windows 11 machine. The advisory pages returned HTTP 404 at the time of writing.

Netty 4.2.20.Final and 4.1.140.Final fix a session-resumption path that let a client reach an mTLS listener without a certificate when ticket keys were shared across differently configured OpenSSL contexts. Upgrade through the BOM, stop sharing ticket keys across client-auth settings, and opt in to the new DNS defences.

Views are my own and do not represent any employer, past or present.