Fedora Account System
Red Hat Associate
Red Hat Customer
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.ISO_8859_1); 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 (`SP_LENIENT_BYTES`). 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`, `GET_PARAMETER`, `SET_PARAMETER`, `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 `SKIP_INITIAL_LINE_CHARS` 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