CVE-2024-29025 (Java, published 2024-03-25T20:15Z)
Description: value: Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. The `HttpPostRequestDecoder` can be tricked to accumulate data. While the decoder can store items on the disk if configured so, there are no limits to the number of fields the form can have, an attacher can send a chunked post consisting of many small fields that will be accumulated in the `bodyListHttpData` list. The decoder cumulates bytes in the `undecodedChunk` buffer until it can decode a field, this field can cumulate data without limits. This vulnerability is fixed in 4.1.108.Final.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/netty/netty
Files changed:
codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
Commit message: Merge pull request from GHSA-5jpm-x58v-624v

* The InterfaceHttpPostRequestDecoder form implementations does not provide hard limits for the number of fields a form can have and the number of accumulated bytes. The former can be used by sending a large amount of fields that will fill the bodyListHttpData list, the later can be used by sending a very large field filling the undecodedChunk buffer since the decoder implementation buffers the field before handling it.

This provides hard limits for both: maxFields defines the maximum number of fields the form can have, maxBufferedBytes defines the maximum number of bytes a field can cumulate. When a limit is reached, a decoder exception is thrown, letting the decoder controller take care of it.

* Set default limits for maxFields/maxUnbufferedBytes (breaking change)

* Update codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java

* Update codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java

---------

Co-authored-by: Julien Viet <julien@julienviet.com>

Vulnerable code:

Fixed code:
    /**
     * The maximum number of fields allows by the form
     */
    private final int maxFields;

    /**
     * The maximum number of accumulated bytes when decoding a field
     */
    private final int maxBufferedBytes;

        this(factory, request, charset, HttpPostRequestDecoder.DEFAULT_MAX_FIELDS, HttpPostRequestDecoder.DEFAULT_MAX_BUFFERED_BYTES);
    }

    /**
     *
     * @param factory
     *            the factory used to create InterfaceHttpData
     * @param request
     *            the request to decode
     * @param charset
     *            the charset to use as default
     * @param maxFields
     *            the maximum number of fields the form can have, {@code -1} to disable
     * @param maxBufferedBytes
     *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
     * @throws NullPointerException
     *             for request or charset or factory
     * @throws ErrorDataDecoderException
     *             if the default charset was wrong when decoding or other
     *             errors
     */
    public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset,
                                           int maxFields, int maxBufferedBytes) {
        this.maxFields = maxFields;
        this.maxBufferedBytes = maxBufferedBytes;
        if (maxBufferedBytes > 0 && undecodedChunk != null && undecodedChunk.readableBytes() > maxBufferedBytes) {
            throw new HttpPostRequestDecoder.TooLongFormFieldException();
        }
        if (maxFields > 0 && bodyListHttpData.size() >= maxFields) {
            throw new HttpPostRequestDecoder.TooManyFormFieldsException();
        }
    static final int DEFAULT_MAX_FIELDS = 128;

    static final int DEFAULT_MAX_BUFFERED_BYTES = 1024;

    /**
     *
     * @param request
     *            the request to decode
     * @param maxFields
     *            the maximum number of fields the form can have, {@code -1} to disable
     * @param maxBufferedBytes
     *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
     * @throws NullPointerException
     *             for request
     * @throws ErrorDataDecoderException
     *             if the default charset was wrong when decoding or other
     *             errors
     */
    public HttpPostRequestDecoder(HttpRequest request, int maxFields, int maxBufferedBytes) {
        this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET,
             maxFields, maxBufferedBytes);
    }

    /**
     *
     * @param factory
     *            the factory used to create InterfaceHttpData
     * @param request
     *            the request to decode
     * @param charset
     *            the charset to use as default
     * @param maxFields
     *            the maximum number of fields the form can have, {@code -1} to disable
     * @param maxBufferedBytes
     *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
     * @throws NullPointerException
     *             for request or charset or factory
     * @throws ErrorDataDecoderException
     *             if the default charset was wrong when decoding or other
     *             errors
     */
    public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset,
                                  int maxFields, int maxBufferedBytes) {
        ObjectUtil.checkNotNull(factory, "factory");
        ObjectUtil.checkNotNull(request, "request");
        ObjectUtil.checkNotNull(charset, "charset");

        // Fill default values
        if (isMultipart(request)) {
            decoder = new HttpPostMultipartRequestDecoder(factory, request, charset, maxFields, maxBufferedBytes);
        } else {
            decoder = new HttpPostStandardRequestDecoder(factory, request, charset, maxFields, maxBufferedBytes);
        }
    }


    /**
     * Exception when the maximum number of fields for a given form is reached
     */
    public static final class TooManyFormFieldsException extends DecoderException {
        private static final long serialVersionUID = 1336267941020800769L;
    }

    /**
     * Exception when a field content is too long
     */
    public static final class TooLongFormFieldException extends DecoderException {
        private static final long serialVersionUID = 1336267941020800769L;
    }
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.TooManyFormFieldsException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.TooLongFormFieldException;
    /**
     * The maximum number of fields allows by the form
     */
    private final int maxFields;

    /**
     * The maximum number of accumulated bytes when decoding a field
     */
    private final int maxBufferedBytes;

        this(factory, request, charset, HttpPostRequestDecoder.DEFAULT_MAX_FIELDS, HttpPostRequestDecoder.DEFAULT_MAX_BUFFERED_BYTES);
    }

    /**
     *
     * @param factory
     *            the factory used to create InterfaceHttpData
     * @param request
     *            the request to decode
     * @param charset
     *            the charset to use as default
     * @param maxFields
     *            the maximum number of fields the form can have, {@code -1} to disable
     * @param maxBufferedBytes
     *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
     * @throws NullPointerException
     *             for request or charset or factory
     * @throws ErrorDataDecoderException
     *             if the default charset was wrong when decoding or other
     *             errors
     */
    public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset,
                                          int maxFields, int maxBufferedBytes) {
        this.maxFields = maxFields;
        this.maxBufferedBytes = maxBufferedBytes;
        if (maxBufferedBytes > 0 && undecodedChunk != null && undecodedChunk.readableBytes() > maxBufferedBytes) {
            throw new TooLongFormFieldException();
        }
        if (maxFields > 0 && bodyListHttpData.size() >= maxFields) {
            throw new TooManyFormFieldsException();
        }
import io.netty.handler.codec.DecoderException;
    @Test
    public void testTooManyFormFieldsPostStandardDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, 1024, -1);

        int num = 0;
        while (true) {
            try {
                decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer("foo=bar&".getBytes())));
            } catch (DecoderException e) {
                assertEquals(HttpPostRequestDecoder.TooManyFormFieldsException.class, e.getClass());
                break;
            }
            assertTrue(num++ < 1024);
        }
        assertEquals(1024, num);
    }

    @Test
    public void testTooManyFormFieldsPostMultipartDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
        req.headers().add("Content-Type", "multipart/form-data;boundary=be38b42a9ad2713f");

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, 1024, -1);
        decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer("--be38b42a9ad2713f\n".getBytes())));

        int num = 0;
        while (true) {
            try {
                byte[] bodyBytes = ("content-disposition: form-data; name=\"title\"\n" +
                        "content-length: 10\n" +
                        "content-type: text/plain; charset=UTF-8\n" +
                        "\n" +
                        "bar-stream\n" +
                        "--be38b42a9ad2713f\n").getBytes();
                ByteBuf content = Unpooled.wrappedBuffer(bodyBytes);
                decoder.offer(new DefaultHttpContent(content));
            } catch (DecoderException e) {
                assertEquals(HttpPostRequestDecoder.TooManyFormFieldsException.class, e.getClass());
                break;
            }
            assertTrue(num++ < 1024);
        }
        assertEquals(1024, num);
    }

    @Test
    public void testTooLongFormFieldStandardDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, -1, 16 * 1024);

        try {
            decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer(new byte[16 * 1024 + 1])));
            fail();
        } catch (DecoderException e) {
            assertEquals(HttpPostRequestDecoder.TooLongFormFieldException.class, e.getClass());
        }
    }

    @Test
    public void testFieldGreaterThanMaxBufferedBytesStandardDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, -1, 6);

        decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer("foo=bar".getBytes())));
    }

    @Test
    public void testTooLongFormFieldMultipartDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
        req.headers().add("Content-Type", "multipart/form-data;boundary=be38b42a9ad2713f");

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, -1, 16 * 1024);

        try {
            decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer(new byte[16 * 1024 + 1])));
            fail();
        } catch (DecoderException e) {
            assertEquals(HttpPostRequestDecoder.TooLongFormFieldException.class, e.getClass());
        }
    }

    @Test
    public void testFieldGreaterThanMaxBufferedBytesMultipartDecoder() {
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
        req.headers().add("Content-Type", "multipart/form-data;boundary=be38b42a9ad2713f");

        byte[] bodyBytes = ("content-disposition: form-data; name=\"title\"\n" +
                "content-length: 10\n" +
                "content-type: text/plain; charset=UTF-8\n" +
                "\n" +
                "bar-stream\n" +
                "--be38b42a9ad2713f\n").getBytes();

        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req, -1, bodyBytes.length - 1);

        decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer(bodyBytes)));
    }