CVE-2026-93565: Io.netty/netty-codec-http: netty rtspdecoder method-token smuggling via trailing control byte

Published Sep 18, 2026
·
Updated

A flaw was found in Netty RtspDecoder. The RtspMethods.valueOf() function incorrectly strips trailing control bytes from method tokens in Real-Time Streaming Protocol (RTSP) requests. A remote attacker can exploit this by sending a specially crafted RTSP request, leading to method-token smuggling. This vulnerability allows an attacker to bypass method-based access controls and can also be used to launder malicious requests through Netty-based RTSP proxies, making them appear legitimate to backend systems.

Other sources

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

— Red Hat

Affected Software

2 affected components
Netty netty-codec-http>=4.2.0.Final<=4.2.17.Final
Netty netty-codec-http=4.1.137.Final

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade maven:io.netty:netty-codec-http to a version that resolves this vulnerability.

    Fixed in 4.1.137.Final; fixed unknown
  2. Compensating control

    For any Netty-based RTSP server/proxy that makes authorization/routing decisions based on request.method(), add an out-of-band validation layer for the raw RTSP method token so that tokens containing trailing control bytes (e.g., "PLAY\x00" or "PLAY\r" immediately before the separating space) are rejected before Netty's RtspDecoder result is used for access control or routing.

Event History

Sep 18, 2026
Data Sourced
via Red Hat·10:00 AM
DescriptionSeverityAffected Software
CVE Published
via MITRE·02:18 PM
Data Sourced
via MITRE·02:18 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·03:17 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

What traffic and deployment conditions make this relevant?

The issue is relevant to Netty-based RTSP servers that use RtspDecoder to process wire-delivered RTSP requests. An attacker can send a method token with a trailing control byte immediately before the separating space, such as PLAY followed by a null byte or carriage return.

2

Does exploitation require authentication or user interaction?

No. The supplied severity vector indicates network reachability, low attack complexity, no privileges required, and no user interaction.

3

How would a malformed request appear to the application?

A request such as PLAY followed by a trailing control byte is decoded as a successful request with decoderResult().isSuccess() true. Its method is RtspMethods.PLAY, using the same cached singleton reference as a clean PLAY request, so the application layer cannot distinguish it from a normal PLAY request.

Contact

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