Bug 2536952 (CVE-2026-93565) - CVE-2026-93565 io.netty/netty-codec-http: Netty RtspDecoder Method-Token Smuggling via Trailing Control Byte
Summary: CVE-2026-93565 io.netty/netty-codec-http: Netty RtspDecoder Method-Token Smug...
Keywords:
Status: NEW
Alias: CVE-2026-93565
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
high
high
Target Milestone: ---
Assignee: Product Security
QA Contact:
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-09-18 10:00 UTC by OSIDB Bzimport
Modified: 2026-09-18 20:01 UTC (History)
66 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Embargoed:


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-09-18 10:00:07 UTC
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


Note You need to log in before you can comment on or make changes to this bug.