[Git][java-team/netty][trixie] CVE-2025-67735

Bastien Roucariès (@rouca) gitlab at salsa.debian.org
Mon Feb 9 10:24:58 GMT 2026



Bastien Roucariès pushed to branch trixie at Debian Java Maintainers / netty


Commits:
a02e7b5a by Bastien Roucariès at 2026-02-09T11:24:30+01:00
CVE-2025-67735

- - - - -


3 changed files:

- debian/changelog
- + debian/patches/CVE-2025-67735.patch
- debian/patches/series


Changes:

=====================================
debian/changelog
=====================================
@@ -33,6 +33,14 @@ netty (1:4.1.48-10+deb13u1) trixie-security; urgency=high
     This bypasses standard email authentication and can
     be used to impersonate executives and forge high-stakes
     corporate communications.
+  * Fix CVE-2025-67735 (Closes: #1123606)
+    `io.netty.handler.codec.http.HttpRequestEncoder`
+    has a CRLF injection with the request URI when constructing
+    a request. This leads to request smuggling when
+    `HttpRequestEncoder` is used without proper sanitization
+    of the URI. Any application / framework using `HttpRequestEncoder`
+    can be subject to be abused to perform request smuggling using
+    CRLF injection
 
  -- Bastien Roucariès <rouca at debian.org>  Sat, 15 Nov 2025 10:21:34 +0100
 


=====================================
debian/patches/CVE-2025-67735.patch
=====================================
@@ -0,0 +1,389 @@
+From: Chris Vest <christianvest_hansen at apple.com>
+Date: Thu, 11 Dec 2025 09:20:08 -0800
+Subject: Merge commit from fork * Reject encoding of HTTP URIs that have
+ line-breaks
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+Motivation:
+Line-breaks in user-supplied data can cause security issues like request/response splitting, request smuggling, and parser desynchronization.
+The URI was not being checked for containing line-breaks before encoding.
+
+Modification:
+When encoding the URI in HttpRequestEncoder, we now also check if it contains any line-break characters, and if so, throw an IllegalArgumentException.
+
+Result:
+Line-breaks are now being properly neutralized from the URI in HttpRequestEncoder.
+
+Unfortunately, the performance drops a bit from this check.
+
+Before:
+
+```
+Benchmark                                      Mode  Cnt         Score        Error  Units
+HttpRequestEncoderInsertBenchmark.newEncoder  thrpt   40  10169070.498 ±  27016.445  ops/s
+```
+
+Now:
+
+```
+Benchmark                                      Mode  Cnt         Score       Error  Units
+HttpRequestEncoderInsertBenchmark.newEncoder  thrpt   40   7984846.328 ± 29959.587  ops/s
+```
+
+* Move the request line encoding safety checks to DefaultHttpRequest
+
+origin: backport, https://github.com/netty/netty/commit/77e81f1e5944d98b3acf887d3aa443b252752e94
+---
+ .../handler/codec/http/DefaultFullHttpRequest.java | 10 ++-
+ .../handler/codec/http/DefaultHttpRequest.java     | 16 ++++
+ .../java/io/netty/handler/codec/http/HttpUtil.java | 57 +++++++++++++-
+ .../handler/codec/http/DefaultHttpRequestTest.java | 88 +++++++++++++++++++++-
+ .../handler/codec/http/HttpRequestEncoderTest.java |  2 -
+ .../io/netty/handler/codec/http/HttpUtilTest.java  | 60 ++++++++++++++-
+ 6 files changed, 222 insertions(+), 11 deletions(-)
+
+diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java
+index 117e6db..d599241 100644
+--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java
++++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java
+@@ -53,7 +53,15 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
+ 
+     public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri,
+             ByteBuf content, HttpHeaders headers, HttpHeaders trailingHeader) {
+-        super(httpVersion, method, uri, headers);
++        this(httpVersion, method, uri, content, headers, trailingHeader, true);
++    }
++
++    /**
++     * Create a full HTTP response with the given HTTP version, method, URI, contents, and header and trailer objects.
++     */
++    public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri,
++            ByteBuf content, HttpHeaders headers, HttpHeaders trailingHeader, boolean validateRequestLine) {
++        super(httpVersion, method, uri, headers, validateRequestLine);
+         this.content = checkNotNull(content, "content");
+         this.trailingHeader = checkNotNull(trailingHeader, "trailingHeader");
+     }
+diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
+index dbc7dd3..d0df1c0 100644
+--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
++++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
+@@ -61,9 +61,25 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
+      * @param headers           the Headers for this Request
+      */
+     public DefaultHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, HttpHeaders headers) {
++        this(httpVersion, method, uri, headers, true);
++    }
++
++    /**
++     * Creates a new instance.
++     *
++     * @param httpVersion       the HTTP version of the request
++     * @param method            the HTTP method of the request
++     * @param uri               the URI or path of the request
++     * @param headers           the Headers for this Request
++     */
++    public DefaultHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, HttpHeaders headers,
++                              boolean validateRequestLine) {
+         super(httpVersion, headers);
+         this.method = checkNotNull(method, "method");
+         this.uri = checkNotNull(uri, "uri");
++        if (validateRequestLine) {
++            HttpUtil.validateRequestLineTokens(httpVersion, method, uri);
++        }
+     }
+ 
+     @Override
+diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
+index afa3ec4..512d841 100644
+--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
++++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
+@@ -41,12 +41,13 @@ public final class HttpUtil {
+     private static final AsciiString CHARSET_EQUALS = AsciiString.of(HttpHeaderValues.CHARSET + "=");
+     private static final AsciiString SEMICOLON = AsciiString.cached(";");
+     private static final String COMMA_STRING = String.valueOf(COMMA);
++    private static final long ILLEGAL_REQUEST_LINE_TOKEN_OCTET_MASK = 1L << '\n' | 1L << '\r' | 1L << ' ';
+ 
+     private HttpUtil() { }
+ 
+     /**
+      * Determine if a uri is in origin-form according to
+-     * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
++     * <a href="https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.1">RFC 9112, 3.2.1</a>.
+      */
+     public static boolean isOriginForm(URI uri) {
+         return uri.getScheme() == null && uri.getSchemeSpecificPart() == null &&
+@@ -54,8 +55,8 @@ public final class HttpUtil {
+     }
+ 
+     /**
+-     * Determine if a uri is in asterisk-form according to
+-     * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
++     * Determine if a string uri is in origin-form according to
++     * <a href="https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.1">RFC 9112, 3.2.1</a>.
+      */
+     public static boolean isAsteriskForm(URI uri) {
+         return "*".equals(uri.getPath()) &&
+@@ -475,6 +476,56 @@ public final class HttpUtil {
+         return null;
+     }
+ 
++    static void validateRequestLineTokens(HttpVersion httpVersion, HttpMethod method, String uri) {
++        // The HttpVersion class does its own validation, and it's not possible for subclasses to circumvent it.
++        // The HttpMethod class does its own validation, but subclasses might circumvent it.
++        if (method.getClass() != HttpMethod.class) {
++            if (!isEncodingSafeStartLineToken(method.asciiName())) {
++                throw new IllegalArgumentException(
++                        "The HTTP method name contain illegal characters: " + method.asciiName());
++            }
++        }
++
++        if (!isEncodingSafeStartLineToken(uri)) {
++            throw new IllegalArgumentException("The URI contain illegal characters: " + uri);
++        }
++    }
++
++    /**
++     * Validate that the given request line token is safe for verbatim encoding to the network.
++     * This does not fully check that the token – HTTP method, version, or URI – is valid and formatted correctly.
++     * Only that the token does not contain characters that would break or
++     * desynchronize HTTP message parsing of the start line wherein the token would be included.
++     * <p>
++     * See <a href="https://datatracker.ietf.org/doc/html/rfc9112#name-request-line">RFC 9112, 3.</a>
++     *
++     * @param token The token to check.
++     * @return {@code true} if the token is safe to encode verbatim into the HTTP message output stream,
++     * otherwise {@code false}.
++     */
++    public static boolean isEncodingSafeStartLineToken(CharSequence token) {
++        int i = 0;
++        int lenBytes = token.length();
++        int modulo = lenBytes % 4;
++        int lenInts = modulo == 0 ? lenBytes : lenBytes - modulo;
++        for (; i < lenInts; i += 4) {
++            long chars = 1L << token.charAt(i) |
++                    1L << token.charAt(i + 1) |
++                    1L << token.charAt(i + 2) |
++                    1L << token.charAt(i + 3);
++            if ((chars & ILLEGAL_REQUEST_LINE_TOKEN_OCTET_MASK) != 0) {
++                return false;
++            }
++        }
++        for (; i < lenBytes; i++) {
++            long ch = 1L << token.charAt(i);
++            if ((ch & ILLEGAL_REQUEST_LINE_TOKEN_OCTET_MASK) != 0) {
++                return false;
++            }
++        }
++        return true;
++    }
++
+     /**
+      * Fetch MIME type part from message's Content-Type header as a char sequence.
+      *
+diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpRequestTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpRequestTest.java
+index cf0fa92..7f6e1a1 100644
+--- a/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpRequestTest.java
++++ b/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpRequestTest.java
+@@ -22,8 +22,94 @@ import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
+ import static org.junit.Assert.assertNull;
+ import static org.junit.Assert.assertTrue;
+ 
+-public class DefaultHttpRequestTest {
++import org.junit.runner.RunWith;
++import org.junit.runners.Parameterized;
++import org.junit.runners.Parameterized.Parameters;
++
++import java.util.Arrays;
++import java.util.Collection;
++
++import static org.junit.Assert.fail;
++
++ at RunWith(Parameterized.class)
++class DefaultHttpRequestIllegalMethodTest {
++    
++    private final String method;
++    
++    public DefaultHttpRequestIllegalMethodTest(String method) {
++        this.method = method;
++    }
++    
++    @Parameters(name = "{index}: method={0}")
++    public static Collection<Object[]> data() {
++        return Arrays.asList(new Object[][] {
++            {"GET "}, {" GET"}, {"G ET"}, {" GET "}, {"GET\r"}, {"GET\n"},
++            {"GET\r\n"}, {"GE\rT"}, {"GE\nT"}, {"GE\r\nT"}, {"\rGET"},
++            {"\nGET"}, {"\r\nGET"}, {" \r\nGET"}, {"\r \nGET"}, {"\r\n GET"},
++            {"\r\nGET "}, {"\nGET "}, {"\rGET "}, {"\r GET"}, {" \rGET"},
++            {"\nGET "}, {"\n GET"}, {" \nGET"}, {"GET \n"}, {"GET \r"},
++            {" GET\r"}, {" GET\r"}, {"GET \n"}, {" GET\n"}, {" GET\n"},
++            {"GE\nT "}, {"GE\rT "}, {" GE\rT"}, {" GE\rT"}, {"GE\nT "},
++            {" GE\nT"}, {" GE\nT"}
++        });
++    }
++    
++    @Test
++    public void constructorMustRejectIllegalHttpMethodByDefault() {
++        try {
++            new DefaultHttpRequest(HttpVersion.HTTP_1_0, new HttpMethod("GET") {
++                public AsciiString asciiName() {
++                    return new AsciiString(method);
++                }
++            }, "/");
++            fail("Expected IllegalArgumentException for method: " + method);
++        } catch (IllegalArgumentException e) {
++            // Expected exception
++        }
++    }
++}
+ 
++ at RunWith(Parameterized.class)
++class DefaultHttpRequestIllegalUriTest {
++    
++    private final String uri;
++    
++    public DefaultHttpRequestIllegalUriTest(String uri) {
++        this.uri = uri;
++    }
++    
++    @Parameters(name = "{index}: uri={0}")
++    public static Collection<Object[]> data() {
++        return Arrays.asList(new Object[][] {
++            {"http://localhost/\r\n"},
++            {"/r\r\n?q=1"},
++            {"http://localhost/\r\n?q=1"},
++            {"/r\r\n/?q=1"},
++            {"http://localhost/\r\n/?q=1"},
++            {"/r\r\n"},
++            {"http://localhost/ HTTP/1.1\r\n\r\nPOST /p HTTP/1.1\r\n\r\n"},
++            {"/r HTTP/1.1\r\n\r\nPOST /p HTTP/1.1\r\n\r\n"},
++            {"/ path"},
++            {"/path "},
++            {" /path"},
++            {"http://localhost/ "},
++            {" http://localhost/"},
++            {"http://local host/"}
++        });
++    }
++    
++    @Test
++    public void constructorMustRejectIllegalUrisByDefault() {
++        try {
++            new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri);
++            fail("Expected IllegalArgumentException for URI: " + uri);
++        } catch (IllegalArgumentException e) {
++            // Expected exception
++        }
++    }
++}
++
++public class DefaultHttpRequestTest {
+     @Test
+     public void testHeaderRemoval() {
+         HttpMessage m = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
+diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
+index 2f866f7..7008b4b 100644
+--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
++++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
+@@ -32,8 +32,6 @@ import static org.hamcrest.Matchers.instanceOf;
+ import static org.hamcrest.Matchers.is;
+ import static org.junit.Assert.*;
+ 
+-/**
+- */
+ public class HttpRequestEncoderTest {
+ 
+     @SuppressWarnings("deprecation")
+diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
+index 186b498..332f57f 100644
+--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
++++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
+@@ -15,17 +15,22 @@
+  */
+ package io.netty.handler.codec.http;
+ 
++import io.netty.util.CharsetUtil;
++import io.netty.util.ReferenceCountUtil;
++import io.netty.handler.codec.http.HttpHeaders;
+ import java.net.InetAddress;
+ import java.net.InetSocketAddress;
++import java.net.URI;
+ import java.nio.charset.StandardCharsets;
+ import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
+ import java.util.Collections;
+ import java.util.List;
+-
+-import io.netty.util.CharsetUtil;
+-import io.netty.util.ReferenceCountUtil;
++import org.junit.runner.RunWith;
++import org.junit.runners.Parameterized;
++import org.junit.runners.Parameterized.Parameters;
+ import org.junit.Test;
+-
+ import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
+ import static org.junit.Assert.assertEquals;
+ import static org.junit.Assert.assertFalse;
+@@ -33,6 +38,41 @@ import static org.junit.Assert.assertNull;
+ import static org.junit.Assert.assertTrue;
+ import static org.junit.Assert.fail;
+ 
++ at RunWith(Parameterized.class)
++class HttpUtilRequestLineTokenValidationTest {
++    
++    private final String token;
++    
++    public HttpUtilRequestLineTokenValidationTest(String token) {
++        this.token = token;
++    }
++    
++    @Parameters(name = "{index}: token={0}")
++    public static Collection<Object[]> data() {
++        return Arrays.asList(new Object[][] {
++            {"http://localhost/\r\n"},
++            {"/r\r\n?q=1"},
++            {"http://localhost/\r\n?q=1"},
++            {"/r\r\n/?q=1"},
++            {"http://localhost/\r\n/?q=1"},
++            {"/r\r\n"},
++            {"http://localhost/ HTTP/1.1\r\n\r\nPOST /p HTTP/1.1\r\n\r\n"},
++            {"/r HTTP/1.1\r\n\r\nPOST /p HTTP/1.1\r\n\r\n"},
++            {"GET "},
++            {" GET"},
++            {"HTTP/ 1.1"},
++            {"HTTP/\r0.9"},
++            {"HTTP/\n1.1"}
++        });
++    }
++    
++    @Test
++    public void requestLineTokenValidationMustRejectInvalidTokens() throws Exception {
++        assertFalse(HttpUtil.isEncodingSafeStartLineToken(token));
++    }
++}
++
++
+ public class HttpUtilTest {
+ 
+     @Test
+@@ -59,6 +99,18 @@ public class HttpUtilTest {
+         assertEquals("2", values.get(1));
+     }
+ 
++    @Test
++    public void testRecognizesAsteriskForm() {
++        // Asterisk form: https://tools.ietf.org/html/rfc7230#section-5.3.4
++        assertTrue(HttpUtil.isAsteriskForm(URI.create("*")));
++        // Origin form: https://tools.ietf.org/html/rfc7230#section-5.3.1
++        assertFalse(HttpUtil.isAsteriskForm(URI.create("/where?q=now")));
++        // Absolute form: https://tools.ietf.org/html/rfc7230#section-5.3.2
++        assertFalse(HttpUtil.isAsteriskForm(URI.create("http://www.example.org/pub/WWW/TheProject.html")));
++        // Authority form: https://tools.ietf.org/html/rfc7230#section-5.3.3
++        assertFalse(HttpUtil.isAsteriskForm(URI.create("www.example.com:80")));
++    }
++
+     @Test
+     public void testGetCharsetAsRawCharSequence() {
+         String QUOTES_CHARSET_CONTENT_TYPE = "text/html; charset=\"utf8\"";


=====================================
debian/patches/series
=====================================
@@ -31,3 +31,4 @@ CVE-2025-55163_1.patch
 CVE-2025-55163_2.patch
 CVE-2025-58057.patch
 CVE-2025-58056.patch
+CVE-2025-67735.patch



View it on GitLab: https://salsa.debian.org/java-team/netty/-/commit/a02e7b5a4fc7531cb32ab2b35df4a3f2ba826a34

-- 
View it on GitLab: https://salsa.debian.org/java-team/netty/-/commit/a02e7b5a4fc7531cb32ab2b35df4a3f2ba826a34
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20260209/382c6959/attachment.htm>


More information about the pkg-java-commits mailing list