412 lines
18 KiB
Diff
412 lines
18 KiB
Diff
From: Markus Koschany <apo@debian.org>
|
|
Date: Sat, 11 May 2024 21:52:15 +0200
|
|
Subject: CVE-2024-29025
|
|
|
|
Bug-Debian: https://bugs.debian.org/1068110
|
|
Origin: https://github.com/netty/netty/commit/0d0c6ed782d13d423586ad0c71737b2c7d02058c
|
|
---
|
|
.../HttpPostMultipartRequestDecoder.java | 41 +++++++
|
|
.../multipart/HttpPostRequestDecoder.java | 70 ++++++++++++
|
|
.../HttpPostStandardRequestDecoder.java | 44 ++++++++
|
|
.../multipart/HttpPostRequestDecoderTest.java | 103 ++++++++++++++++++
|
|
4 files changed, 258 insertions(+)
|
|
|
|
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
|
|
index 17c3e64..7cac6a0 100644
|
|
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
|
|
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
|
|
@@ -61,6 +61,16 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
|
|
*/
|
|
private final HttpRequest request;
|
|
|
|
+ /**
|
|
+ * 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;
|
|
+
|
|
/**
|
|
* Default charset to use
|
|
*/
|
|
@@ -172,9 +182,34 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
|
|
* errors
|
|
*/
|
|
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
|
|
+ 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.request = checkNotNull(request, "request");
|
|
this.charset = checkNotNull(charset, "charset");
|
|
this.factory = checkNotNull(factory, "factory");
|
|
+ this.maxFields = maxFields;
|
|
+ this.maxBufferedBytes = maxBufferedBytes;
|
|
// Fill default values
|
|
|
|
setMultipart(this.request.headers().get(HttpHeaderNames.CONTENT_TYPE));
|
|
@@ -333,6 +368,9 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
|
|
isLastChunk = true;
|
|
}
|
|
parseBody();
|
|
+ if (maxBufferedBytes > 0 && undecodedChunk != null && undecodedChunk.readableBytes() > maxBufferedBytes) {
|
|
+ throw new HttpPostRequestDecoder.TooLongFormFieldException();
|
|
+ }
|
|
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
|
|
undecodedChunk.discardReadBytes();
|
|
}
|
|
@@ -417,6 +455,9 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
|
|
if (data == null) {
|
|
return;
|
|
}
|
|
+ if (maxFields > 0 && bodyListHttpData.size() >= maxFields) {
|
|
+ throw new HttpPostRequestDecoder.TooManyFormFieldsException();
|
|
+ }
|
|
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
|
|
if (datas == null) {
|
|
datas = new ArrayList<InterfaceHttpData>(1);
|
|
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
|
|
index 0c10626..d57b63e 100644
|
|
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
|
|
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
|
|
@@ -25,6 +25,7 @@ import io.netty.util.internal.StringUtil;
|
|
|
|
import java.nio.charset.Charset;
|
|
import java.util.List;
|
|
+import io.netty.util.internal.ObjectUtil;
|
|
|
|
/**
|
|
* This decoder will decode Body and can handle POST BODY.
|
|
@@ -36,6 +37,10 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
|
|
|
|
static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
|
|
|
|
+ static final int DEFAULT_MAX_FIELDS = 128;
|
|
+
|
|
+ static final int DEFAULT_MAX_BUFFERED_BYTES = 1024;
|
|
+
|
|
private final InterfaceHttpPostRequestDecoder decoder;
|
|
|
|
/**
|
|
@@ -52,6 +57,25 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
|
|
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
|
|
}
|
|
|
|
+ /**
|
|
+ *
|
|
+ * @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
|
|
@@ -100,6 +124,38 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
|
|
}
|
|
}
|
|
|
|
+ /**
|
|
+ *
|
|
+ * @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);
|
|
+ }
|
|
+ }
|
|
+
|
|
/**
|
|
* states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD |
|
|
* FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER
|
|
@@ -342,4 +398,18 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
|
|
super(msg, cause);
|
|
}
|
|
}
|
|
+
|
|
+ /**
|
|
+ * 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;
|
|
+ }
|
|
}
|
|
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
|
|
index ece64d8..65a9e16 100644
|
|
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
|
|
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
|
|
@@ -16,6 +16,7 @@
|
|
package io.netty.handler.codec.http.multipart;
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
+import io.netty.handler.codec.DecoderException;
|
|
import io.netty.handler.codec.http.HttpConstants;
|
|
import io.netty.handler.codec.http.HttpContent;
|
|
import io.netty.handler.codec.http.HttpRequest;
|
|
@@ -26,6 +27,8 @@ import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDec
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
|
|
+import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.TooManyFormFieldsException;
|
|
+import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.TooLongFormFieldException;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.charset.Charset;
|
|
@@ -60,6 +63,16 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
|
|
*/
|
|
private final Charset charset;
|
|
|
|
+ /**
|
|
+ * 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;
|
|
+
|
|
/**
|
|
* Does the last chunk already received
|
|
*/
|
|
@@ -145,9 +158,34 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
|
|
* errors
|
|
*/
|
|
public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
|
|
+ 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.request = checkNotNull(request, "request");
|
|
this.charset = checkNotNull(charset, "charset");
|
|
this.factory = checkNotNull(factory, "factory");
|
|
+ this.maxFields = maxFields;
|
|
+ this.maxBufferedBytes = maxBufferedBytes;
|
|
if (request instanceof HttpContent) {
|
|
// Offer automatically if the given request is als type of HttpContent
|
|
// See #1089
|
|
@@ -287,6 +325,9 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
|
|
isLastChunk = true;
|
|
}
|
|
parseBody();
|
|
+ if (maxBufferedBytes > 0 && undecodedChunk != null && undecodedChunk.readableBytes() > maxBufferedBytes) {
|
|
+ throw new TooLongFormFieldException();
|
|
+ }
|
|
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
|
|
undecodedChunk.discardReadBytes();
|
|
}
|
|
@@ -367,6 +408,9 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
|
|
if (data == null) {
|
|
return;
|
|
}
|
|
+ if (maxFields > 0 && bodyListHttpData.size() >= maxFields) {
|
|
+ throw new TooManyFormFieldsException();
|
|
+ }
|
|
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
|
|
if (datas == null) {
|
|
datas = new ArrayList<InterfaceHttpData>(1);
|
|
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
|
|
index 1334107..bdc5fd7 100644
|
|
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
|
|
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
|
|
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.ByteBufAllocator;
|
|
import io.netty.buffer.Unpooled;
|
|
import io.netty.buffer.UnpooledByteBufAllocator;
|
|
+import io.netty.handler.codec.DecoderException;
|
|
import io.netty.handler.codec.DecoderResult;
|
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
|
import io.netty.handler.codec.http.DefaultHttpContent;
|
|
@@ -491,4 +492,106 @@ public class HttpPostRequestDecoderTest {
|
|
content.release();
|
|
}
|
|
}
|
|
+
|
|
+ @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)));
|
|
+ }
|
|
}
|
|
--
|
|
2.47.0
|
|
|