REDHAT-BUG-2536952: High severity Netty maven/io.netty/netty-codec-rtsp vulnerability
Netty RtspDecoder Method-Token Smuggling via Trailing Control Byte
A public GitHub Security Advisory (GHSA-h75q-xqrh-59rf) describes the following issue:
Summary RtspMethods.valueOf() silently strips trailing control bytes (any character with code point <= 0x20, the full range that String.trim() removes) before performing a cache lookup against its ten pre-populated method constants. A wire-delivered RTSP request whose method token ends with a trailing control byte — for example PLAY\x00 or PLAY\r, immediately before the separating space — is decoded by RtspDecoder as a fully successful PLAY request, with decoderResult().isSuccess() == true and request.method() == RtspMethods.PLAY (same object reference as the cached singleton). The application layer cannot distinguish this from a clean PLAY request.
This is the same root cause as #16723 and #16971, in a sibling that those fixes did not reach. The fix for HttpMethod hardened HttpMethod.valueOf() directly, but RtspMethods.valueOf() has its own independent checkNonEmptyAfterTrim() call that runs before the cache lookup — meaning a trailing-control-byte token hits the cache before the hardened HttpMethod constructor ever sees it.
Reproduction
Minimal wire-level reproduction
Send the following raw bytes to any Netty-based RTSP server using RtspDecoder:
PLAY\x00 rtsp://target/stream RTSP/1.0\r\n CSeq: 1\r\n \r\n
The \x00 is a literal NUL byte (0x00) immediately before the space that separates the method from the URI. \r (0x0D) produces the same outcome.
Expected (correct) behavior: decode failure, decoderResult().isSuccess() == false. Actual behavior: successful decode, request.method() returns the RtspMethods.PLAY singleton.
Confirmed via EmbeddedChannel test
java byte[] data = ("PLAY\u0000 rtsp://172.20.184.218:554/stream RTSP/1.0\r\n" + "CSeq: 1\r\n\r\n") .getBytes(StandardCharsets.ISO88591);
EmbeddedChannel ch = new EmbeddedChannel(new RtspDecoder()); ch.writeInbound(Unpooled.wrappedBuffer(data));
HttpObject res = ch.readInbound(); // res instanceof HttpRequest → true // request.decoderResult().isSuccess() → TRUE (should be false) // request.method() == RtspMethods.PLAY → TRUE (same reference — cache hit)
Run against netty/netty branch 4.2 at HEAD 775ad710da:
DEBUG decoderResult = success DEBUG method = PLAY DEBUG method == RtspMethods.PLAY (same ref)? true
Root cause
The vulnerable path in RtspMethods.valueOf() (line 127, RtspMethods.java): java
public static HttpMethod valueOf(String name) { name = checkNonEmptyAfterTrim(name, "name").toUpperCase(Locale.US); HttpMethod result = methodMap.get(name); if (result != null) { return result; // <-- cache hit; hardened HttpMethod constructor never runs } else { return HttpMethod.valueOf(name); // hardened path — too late for cached names } }
ObjectUtil.checkNonEmptyAfterTrim() is defined as: java public static String checkNonEmptyAfterTrim(final String value, final String name) { String trimmed = checkNotNull(value, name).trim(); return checkNonEmpty(trimmed, name); }
String.trim() strips every character with code point <= 0x20 from both the leading and trailing ends. A token of "PLAY\u0000" (5 chars) becomes "PLAY" (4 chars), matches the cache key, and returns RtspMethods.PLAY without ever reaching HttpMethod's constructor, which was hardened in #16723 to reject exactly this class of byte.
Why splitInitialLine does not filter this
The base decoder's splitInitialLine tokenises the request line on space-class separators (SP, HT, VT, FF, CR). NUL (0x00) is not in the separator table (SPLENIENTBYTES). A token of "PLAY\u0000" is therefore extracted as a 5-character string with the NUL fully intact, and handed verbatim to RtspDecoder.createMessage() → RtspMethods.valueOf(). The NUL is only stripped by trim() inside checkNonEmptyAfterTrim, at which point the cache lookup has already been set up to succeed.
All ten cached RTSP method names are affected by the trailing-edge placement: DESCRIBE, ANNOUNCE, SETUP, PLAY, PAUSE, TEARDOWN, GETPARAMETER, SETPARAMETER, REDIRECT, RECORD.
Impact Direct: method-based access control bypass
Any Netty-based RTSP server or proxy that makes authorization or routing decisions based on request.method() is vulnerable to having those decisions bypassed. An attacker sends SETUP\x00 or PLAY\x00 where the application's ACL layer would have rejected a clean SETUP or PLAY, but RtspDecoder delivers a successfully-decoded request carrying the trusted cached singleton.
Proxy laundering
When a Netty-based RTSP proxy receives PLAY\x00 ... and re-encodes it for forwarding, RtspEncoder calls request.method().asciiName() — which returns the clean ASCII name from the cached singleton. The backend server receives a completely clean PLAY with no trace of the original NUL. Upstream WAFs or logging infrastructure that saw the raw PLAY\x00 may flag or log it, but anything downstream of the Netty decoder sees a legitimate request and cannot reconstruct that the original token was malformed.
Not affected
HttpServerCodec / HttpRequestDecoder (Spring WebFlux and all HTTP/1.1 Netty servers): createMessage calls HttpMethod.valueOf() directly, not through RtspMethods. Not in scope.
HTTP/2 and HTTP/3 pipelines: independent header validation, not affected. RtspVersions.valueOf(): correctly fixed in #16971, no trim() call present.
Keep-alive connections / pipelined requests: the SKIPINITIALLINECHARS guard re-enters via resetNow() between messages and applies identically to every request on a keep-alive connection — confirmed by test feeding a clean first message followed by a leading-NUL second message on the same EmbeddedChannel.
the second message is rejected by the same InvalidLineSeparatorException path as a first-message leading-NUL. The trai
[truncated]
Affected: - maven:io.netty:netty-codec-http affected >=4.2.0.Final, <=4.2.17.Final; fixed unknown - maven:io.netty:netty-codec-http affected <=4.1.137.Final; fixed unknown
Fixed versions: see advisory
Advisory: https://github.com/netty/netty/security/advisories/GHSA-h75q-xqrh-59rf
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
maven:io.netty:netty-codec-httpto a version that resolves this vulnerability.Fixed in 4.2.0.Final - Upgrade
Upgrade
maven:io.netty:netty-codec-httpto a version that resolves this vulnerability.Fixed in 4.2.17.Final - Upgrade
Upgrade
maven:io.netty:netty-codec-httpto a version that resolves this vulnerability.Fixed in 4.1.137.Final - Compensating control
If you have a Netty-based RTSP server/proxy that makes authorization or routing decisions based on request.method(), add an external control to ensure those decisions are not based solely on RtspMethods.valueOf() / cached method-token results from RtspDecoder (e.g., perform additional validation of the raw RTSP method token before decoder output is trusted).
Event History
Frequently Asked Questions
What input is required to trigger the issue?
An attacker needs to send an RTSP request whose method token has a trailing control byte with a code point of 0x20 or lower immediately before the separating space, such as PLAY followed by a null byte or carriage return.
Can an application detect the malformed method after Netty decodes it?
No. The decoder treats the request as successful and exposes the cached RtspMethods.PLAY method object for a token such as PLAY followed by a trailing control byte, making it indistinguishable from a clean PLAY request at the application layer.
How can I test whether this behavior is present?
Send an RTSP request containing a method token such as PLAY followed by a null byte or carriage return before the space. The affected behavior is indicated if RtspDecoder reports a successful decoder result and the request method resolves to RtspMethods.PLAY.