538 lines
24 KiB
Diff
538 lines
24 KiB
Diff
|
|
From: Markus Koschany <apo@debian.org>
|
||
|
|
Date: Tue, 26 Sep 2023 20:37:47 +0200
|
||
|
|
Subject: CVE-2023-26048
|
||
|
|
|
||
|
|
Origin: https://github.com/eclipse/jetty.project/pull/9345
|
||
|
|
---
|
||
|
|
.../jetty/http/MultiPartFormInputStream.java | 76 +++++++-----
|
||
|
|
.../java/org/eclipse/jetty/server/MultiParts.java | 14 ++-
|
||
|
|
.../java/org/eclipse/jetty/server/Request.java | 127 ++++++++++++---------
|
||
|
|
.../jetty/server/handler/ContextHandler.java | 4 +
|
||
|
|
.../jetty/util/MultiPartInputStreamParser.java | 24 +++-
|
||
|
|
5 files changed, 158 insertions(+), 87 deletions(-)
|
||
|
|
|
||
|
|
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
|
||
|
|
index 928f59c..a1092f7 100644
|
||
|
|
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
|
||
|
|
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java
|
||
|
|
@@ -60,11 +60,14 @@ import org.eclipse.jetty.util.log.Logger;
|
||
|
|
public class MultiPartFormInputStream
|
||
|
|
{
|
||
|
|
private static final Logger LOG = Log.getLogger(MultiPartFormInputStream.class);
|
||
|
|
+ private static final int DEFAULT_MAX_FORM_KEYS = 1000;
|
||
|
|
private static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
|
||
|
|
+ private final MultiMap<Part> _parts;
|
||
|
|
+ private final int _maxParts;
|
||
|
|
+ private int _numParts = 0;
|
||
|
|
private InputStream _in;
|
||
|
|
private MultipartConfigElement _config;
|
||
|
|
private String _contentType;
|
||
|
|
- private MultiMap<Part> _parts;
|
||
|
|
private Throwable _err;
|
||
|
|
private File _tmpDir;
|
||
|
|
private File _contextTmpDir;
|
||
|
|
@@ -332,26 +335,42 @@ public class MultiPartFormInputStream
|
||
|
|
* @param contextTmpDir javax.servlet.context.tempdir
|
||
|
|
*/
|
||
|
|
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
|
||
|
|
+ {
|
||
|
|
+ this(in, contentType, config, contextTmpDir, DEFAULT_MAX_FORM_KEYS);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ /**
|
||
|
|
+ * @param in Request input stream
|
||
|
|
+ * @param contentType Content-Type header
|
||
|
|
+ * @param config MultipartConfigElement
|
||
|
|
+ * @param contextTmpDir javax.servlet.context.tempdir
|
||
|
|
+ * @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
|
||
|
|
+ */
|
||
|
|
+ public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
|
||
|
|
+
|
||
|
|
{
|
||
|
|
_contentType = contentType;
|
||
|
|
_config = config;
|
||
|
|
_contextTmpDir = contextTmpDir;
|
||
|
|
+ _maxParts = maxParts;
|
||
|
|
if (_contextTmpDir == null)
|
||
|
|
_contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
|
||
|
|
|
||
|
|
if (_config == null)
|
||
|
|
_config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
|
||
|
|
|
||
|
|
+ MultiMap<Part> parts = new MultiMap<>();
|
||
|
|
if (in instanceof ServletInputStream)
|
||
|
|
{
|
||
|
|
if (((ServletInputStream)in).isFinished())
|
||
|
|
{
|
||
|
|
- _parts = EMPTY_MAP;
|
||
|
|
+ parts = EMPTY_MAP;
|
||
|
|
_parsed = true;
|
||
|
|
- return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
- _in = new BufferedInputStream(in);
|
||
|
|
+ if (!_parsed)
|
||
|
|
+ _in = new BufferedInputStream(in);
|
||
|
|
+ _parts = parts;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
@@ -495,16 +514,15 @@ public class MultiPartFormInputStream
|
||
|
|
if (_parsed)
|
||
|
|
return;
|
||
|
|
_parsed = true;
|
||
|
|
-
|
||
|
|
+
|
||
|
|
+ MultiPartParser parser = null;
|
||
|
|
+ Handler handler = new Handler();
|
||
|
|
try
|
||
|
|
{
|
||
|
|
- // initialize
|
||
|
|
- _parts = new MultiMap<>();
|
||
|
|
-
|
||
|
|
// if its not a multipart request, don't parse it
|
||
|
|
if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
|
||
|
|
return;
|
||
|
|
-
|
||
|
|
+
|
||
|
|
// sort out the location to which to write the files
|
||
|
|
if (_config.getLocation() == null)
|
||
|
|
_tmpDir = _contextTmpDir;
|
||
|
|
@@ -518,10 +536,10 @@ public class MultiPartFormInputStream
|
||
|
|
else
|
||
|
|
_tmpDir = new File(_contextTmpDir, _config.getLocation());
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
if (!_tmpDir.exists())
|
||
|
|
_tmpDir.mkdirs();
|
||
|
|
-
|
||
|
|
+
|
||
|
|
String contentTypeBoundary = "";
|
||
|
|
int bstart = _contentType.indexOf("boundary=");
|
||
|
|
if (bstart >= 0)
|
||
|
|
@@ -530,22 +548,19 @@ public class MultiPartFormInputStream
|
||
|
|
bend = (bend < 0 ? _contentType.length() : bend);
|
||
|
|
contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart, bend)).trim());
|
||
|
|
}
|
||
|
|
-
|
||
|
|
- Handler handler = new Handler();
|
||
|
|
- MultiPartParser parser = new MultiPartParser(handler, contentTypeBoundary);
|
||
|
|
-
|
||
|
|
+
|
||
|
|
+ parser = new MultiPartParser(handler, contentTypeBoundary);
|
||
|
|
byte[] data = new byte[_bufferSize];
|
||
|
|
int len;
|
||
|
|
long total = 0;
|
||
|
|
-
|
||
|
|
+
|
||
|
|
while (true)
|
||
|
|
{
|
||
|
|
-
|
||
|
|
+
|
||
|
|
len = _in.read(data);
|
||
|
|
-
|
||
|
|
+
|
||
|
|
if (len > 0)
|
||
|
|
{
|
||
|
|
-
|
||
|
|
// keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
|
||
|
|
total += len;
|
||
|
|
if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
|
||
|
|
@@ -553,30 +568,28 @@ public class MultiPartFormInputStream
|
||
|
|
_err = new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
ByteBuffer buffer = BufferUtil.toBuffer(data);
|
||
|
|
buffer.limit(len);
|
||
|
|
if (parser.parse(buffer, false))
|
||
|
|
break;
|
||
|
|
-
|
||
|
|
+
|
||
|
|
if (buffer.hasRemaining())
|
||
|
|
throw new IllegalStateException("Buffer did not fully consume");
|
||
|
|
-
|
||
|
|
}
|
||
|
|
else if (len == -1)
|
||
|
|
{
|
||
|
|
parser.parse(BufferUtil.EMPTY_BUFFER, true);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
-
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
// check for exceptions
|
||
|
|
if (_err != null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
// check we read to the end of the message
|
||
|
|
if (parser.getState() != MultiPartParser.State.END)
|
||
|
|
{
|
||
|
|
@@ -585,19 +598,23 @@ public class MultiPartFormInputStream
|
||
|
|
else
|
||
|
|
_err = new IOException("Incomplete Multipart");
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
if (LOG.isDebugEnabled())
|
||
|
|
{
|
||
|
|
LOG.debug("Parsing Complete {} err={}", parser, _err);
|
||
|
|
}
|
||
|
|
-
|
||
|
|
}
|
||
|
|
catch (Throwable e)
|
||
|
|
{
|
||
|
|
_err = e;
|
||
|
|
+
|
||
|
|
+ // Notify parser if failure occurs
|
||
|
|
+ if (parser != null)
|
||
|
|
+ parser.parse(BufferUtil.EMPTY_BUFFER, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
-
|
||
|
|
+
|
||
|
|
+
|
||
|
|
class Handler implements MultiPartParser.Handler
|
||
|
|
{
|
||
|
|
private MultiPart _part = null;
|
||
|
|
@@ -735,6 +752,9 @@ public class MultiPartFormInputStream
|
||
|
|
public void startPart()
|
||
|
|
{
|
||
|
|
reset();
|
||
|
|
+ _numParts++;
|
||
|
|
+ if (_maxParts >= 0 && _numParts > _maxParts)
|
||
|
|
+ throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
|
||
|
|
index f28b945..1b91649 100644
|
||
|
|
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
|
||
|
|
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java
|
||
|
|
@@ -57,7 +57,12 @@ public interface MultiParts extends Closeable
|
||
|
|
|
||
|
|
public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
|
||
|
|
{
|
||
|
|
- _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir);
|
||
|
|
+ this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
|
||
|
|
+ {
|
||
|
|
+ _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir, maxParts);
|
||
|
|
_context = request.getContext();
|
||
|
|
_httpParser.getParts();
|
||
|
|
}
|
||
|
|
@@ -116,7 +121,12 @@ public interface MultiParts extends Closeable
|
||
|
|
|
||
|
|
public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
|
||
|
|
{
|
||
|
|
- _utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir);
|
||
|
|
+ this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
|
||
|
|
+ {
|
||
|
|
+ _utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir, maxParts);
|
||
|
|
_context = request.getContext();
|
||
|
|
_utilParser.getParts();
|
||
|
|
|
||
|
|
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
|
||
|
|
index 5b996bb..8a7e6f9 100644
|
||
|
|
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
|
||
|
|
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
|
||
|
|
@@ -425,6 +425,14 @@ public class Request implements HttpServletRequest
|
||
|
|
return parameters==null?NO_PARAMS:parameters;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ private boolean isContentEncodingSupported()
|
||
|
|
+ {
|
||
|
|
+ String contentEncoding = getHttpFields().get(HttpHeader.CONTENT_ENCODING);
|
||
|
|
+ if (contentEncoding == null)
|
||
|
|
+ return true;
|
||
|
|
+ return HttpHeaderValue.IDENTITY.is(contentEncoding);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
/* ------------------------------------------------------------ */
|
||
|
|
private void extractQueryParameters()
|
||
|
|
{
|
||
|
|
@@ -458,33 +466,34 @@ public class Request implements HttpServletRequest
|
||
|
|
{
|
||
|
|
String contentType = getContentType();
|
||
|
|
if (contentType == null || contentType.isEmpty())
|
||
|
|
- _contentParameters=NO_PARAMS;
|
||
|
|
+ _contentParameters = NO_PARAMS;
|
||
|
|
else
|
||
|
|
{
|
||
|
|
- _contentParameters=new MultiMap<>();
|
||
|
|
+ _contentParameters = new MultiMap<>();
|
||
|
|
int contentLength = getContentLength();
|
||
|
|
if (contentLength != 0 && _inputState == __NONE)
|
||
|
|
{
|
||
|
|
- contentType = HttpFields.valueParameters(contentType, null);
|
||
|
|
- if (MimeTypes.Type.FORM_ENCODED.is(contentType) &&
|
||
|
|
+ String baseType = HttpFields.valueParameters(contentType, null);
|
||
|
|
+ if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
|
||
|
|
_channel.getHttpConfiguration().isFormEncodedMethod(getMethod()))
|
||
|
|
{
|
||
|
|
- if (_metaData!=null)
|
||
|
|
+ if (_metaData != null && !isContentEncodingSupported())
|
||
|
|
{
|
||
|
|
- String contentEncoding = getHttpFields().get(HttpHeader.CONTENT_ENCODING);
|
||
|
|
- if (contentEncoding!=null && !HttpHeaderValue.IDENTITY.is(contentEncoding))
|
||
|
|
- throw new BadMessageException(HttpStatus.NOT_IMPLEMENTED_501, "Unsupported Content-Encoding");
|
||
|
|
+ throw new BadMessageException(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Content-Encoding");
|
||
|
|
}
|
||
|
|
+
|
||
|
|
extractFormParameters(_contentParameters);
|
||
|
|
}
|
||
|
|
- else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(contentType) &&
|
||
|
|
- getAttribute(__MULTIPART_CONFIG_ELEMENT) != null &&
|
||
|
|
- _multiParts == null)
|
||
|
|
+ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
|
||
|
|
+ getAttribute(__MULTIPART_CONFIG_ELEMENT) != null &&
|
||
|
|
+ _multiParts == null)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
- if (_metaData!=null && getHttpFields().contains(HttpHeader.CONTENT_ENCODING))
|
||
|
|
- throw new BadMessageException(HttpStatus.NOT_IMPLEMENTED_501,"Unsupported Content-Encoding");
|
||
|
|
+ if (_metaData != null && !isContentEncodingSupported())
|
||
|
|
+ {
|
||
|
|
+ throw new BadMessageException(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Content-Encoding");
|
||
|
|
+ }
|
||
|
|
getParts(_contentParameters);
|
||
|
|
}
|
||
|
|
catch (IOException | ServletException e)
|
||
|
|
@@ -502,57 +511,30 @@ public class Request implements HttpServletRequest
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
- int maxFormContentSize = -1;
|
||
|
|
- int maxFormKeys = -1;
|
||
|
|
+ int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
|
||
|
|
+ int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
|
||
|
|
|
||
|
|
if (_context != null)
|
||
|
|
{
|
||
|
|
- maxFormContentSize = _context.getContextHandler().getMaxFormContentSize();
|
||
|
|
- maxFormKeys = _context.getContextHandler().getMaxFormKeys();
|
||
|
|
+ ContextHandler contextHandler = _context.getContextHandler();
|
||
|
|
+ maxFormContentSize = contextHandler.getMaxFormContentSize();
|
||
|
|
+ maxFormKeys = contextHandler.getMaxFormKeys();
|
||
|
|
}
|
||
|
|
-
|
||
|
|
- if (maxFormContentSize < 0)
|
||
|
|
+ else
|
||
|
|
{
|
||
|
|
- Object obj = _channel.getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize");
|
||
|
|
- if (obj == null)
|
||
|
|
- maxFormContentSize = 200000;
|
||
|
|
- else if (obj instanceof Number)
|
||
|
|
- {
|
||
|
|
- Number size = (Number)obj;
|
||
|
|
- maxFormContentSize = size.intValue();
|
||
|
|
- }
|
||
|
|
- else if (obj instanceof String)
|
||
|
|
- {
|
||
|
|
- maxFormContentSize = Integer.parseInt((String)obj);
|
||
|
|
- }
|
||
|
|
- }
|
||
|
|
-
|
||
|
|
- if (maxFormKeys < 0)
|
||
|
|
- {
|
||
|
|
- Object obj = _channel.getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormKeys");
|
||
|
|
- if (obj == null)
|
||
|
|
- maxFormKeys = 1000;
|
||
|
|
- else if (obj instanceof Number)
|
||
|
|
- {
|
||
|
|
- Number keys = (Number)obj;
|
||
|
|
- maxFormKeys = keys.intValue();
|
||
|
|
- }
|
||
|
|
- else if (obj instanceof String)
|
||
|
|
- {
|
||
|
|
- maxFormKeys = Integer.parseInt((String)obj);
|
||
|
|
- }
|
||
|
|
+ maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
|
||
|
|
+ maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
|
||
|
|
}
|
||
|
|
|
||
|
|
int contentLength = getContentLength();
|
||
|
|
- if (contentLength > maxFormContentSize && maxFormContentSize > 0)
|
||
|
|
- {
|
||
|
|
- throw new IllegalStateException("Form too large: " + contentLength + " > " + maxFormContentSize);
|
||
|
|
- }
|
||
|
|
+ if (maxFormContentSize >= 0 && contentLength > maxFormContentSize)
|
||
|
|
+ throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
|
||
|
|
+
|
||
|
|
InputStream in = getInputStream();
|
||
|
|
if (_input.isAsync())
|
||
|
|
throw new IllegalStateException("Cannot extract parameters with async IO");
|
||
|
|
|
||
|
|
- UrlEncoded.decodeTo(in,params,getCharacterEncoding(),contentLength<0?maxFormContentSize:-1,maxFormKeys);
|
||
|
|
+ UrlEncoded.decodeTo(in, params, getCharacterEncoding(), maxFormContentSize, maxFormKeys);
|
||
|
|
}
|
||
|
|
catch (IOException e)
|
||
|
|
{
|
||
|
|
@@ -561,6 +543,16 @@ public class Request implements HttpServletRequest
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
+ private int lookupServerAttribute(String key, int dftValue)
|
||
|
|
+ {
|
||
|
|
+ Object attribute = _channel.getServer().getAttribute(key);
|
||
|
|
+ if (attribute instanceof Number)
|
||
|
|
+ return ((Number)attribute).intValue();
|
||
|
|
+ else if (attribute instanceof String)
|
||
|
|
+ return Integer.parseInt((String)attribute);
|
||
|
|
+ return dftValue;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
/* ------------------------------------------------------------ */
|
||
|
|
@Override
|
||
|
|
public AsyncContext getAsyncContext()
|
||
|
|
@@ -2351,9 +2343,23 @@ public class Request implements HttpServletRequest
|
||
|
|
if (config == null)
|
||
|
|
throw new IllegalStateException("No multipart config for servlet");
|
||
|
|
|
||
|
|
+ int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
|
||
|
|
+ int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
|
||
|
|
+ if (_context != null)
|
||
|
|
+ {
|
||
|
|
+ ContextHandler contextHandler = _context.getContextHandler();
|
||
|
|
+ maxFormContentSize = contextHandler.getMaxFormContentSize();
|
||
|
|
+ maxFormKeys = contextHandler.getMaxFormKeys();
|
||
|
|
+ }
|
||
|
|
+ else
|
||
|
|
+ {
|
||
|
|
+ maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
|
||
|
|
+ maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
_multiParts = newMultiParts(getInputStream(),
|
||
|
|
getContentType(), config,
|
||
|
|
- (_context != null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null));
|
||
|
|
+ (_context != null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null),maxFormKeys);
|
||
|
|
|
||
|
|
setAttribute(__MULTIPARTS, _multiParts);
|
||
|
|
Collection<Part> parts = _multiParts.getParts(); //causes parsing
|
||
|
|
@@ -2388,11 +2394,16 @@ public class Request implements HttpServletRequest
|
||
|
|
else
|
||
|
|
defaultCharset = StandardCharsets.UTF_8;
|
||
|
|
|
||
|
|
+ long formContentSize = 0;
|
||
|
|
ByteArrayOutputStream os = null;
|
||
|
|
for (Part p:parts)
|
||
|
|
{
|
||
|
|
if (p.getSubmittedFileName() == null)
|
||
|
|
{
|
||
|
|
+ formContentSize = Math.addExact(formContentSize, p.getSize());
|
||
|
|
+ if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
|
||
|
|
+ throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
|
||
|
|
+
|
||
|
|
// Servlet Spec 3.0 pg 23, parts without filename must be put into params.
|
||
|
|
String charset = null;
|
||
|
|
if (p.getContentType() != null)
|
||
|
|
@@ -2418,7 +2429,9 @@ public class Request implements HttpServletRequest
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
- private MultiParts newMultiParts(ServletInputStream inputStream, String contentType, MultipartConfigElement config, Object object) throws IOException
|
||
|
|
+ private MultiParts newMultiParts(ServletInputStream inputStream, String
|
||
|
|
+ contentType, MultipartConfigElement config, Object object,
|
||
|
|
+ int maxParts) throws IOException
|
||
|
|
{
|
||
|
|
MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
|
||
|
|
if(LOG.isDebugEnabled())
|
||
|
|
@@ -2428,12 +2441,14 @@ public class Request implements HttpServletRequest
|
||
|
|
{
|
||
|
|
case RFC7578:
|
||
|
|
return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config,
|
||
|
|
- (_context != null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null), this);
|
||
|
|
+ (_context !=
|
||
|
|
+ null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null), this, maxParts);
|
||
|
|
|
||
|
|
case LEGACY:
|
||
|
|
default:
|
||
|
|
return new MultiParts.MultiPartsUtilParser(getInputStream(), getContentType(), config,
|
||
|
|
- (_context != null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null), this);
|
||
|
|
+ (_context !=
|
||
|
|
+ null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null), this, maxParts);
|
||
|
|
|
||
|
|
}
|
||
|
|
}
|
||
|
|
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
|
||
|
|
index b6ca046..c4cbebd 100644
|
||
|
|
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
|
||
|
|
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
|
||
|
|
@@ -132,6 +132,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||
|
|
*/
|
||
|
|
public static final String MANAGED_ATTRIBUTES = "org.eclipse.jetty.server.context.ManagedAttributes";
|
||
|
|
|
||
|
|
+ public static final String MAX_FORM_KEYS_KEY = "org.eclipse.jetty.server.Request.maxFormKeys";
|
||
|
|
+ public static final String MAX_FORM_CONTENT_SIZE_KEY = "org.eclipse.jetty.server.Request.maxFormContentSize";
|
||
|
|
+ public static final int DEFAULT_MAX_FORM_KEYS = 1000;
|
||
|
|
+ public static final int DEFAULT_MAX_FORM_CONTENT_SIZE = 200000;
|
||
|
|
/* ------------------------------------------------------------ */
|
||
|
|
/**
|
||
|
|
* Get the current ServletContext implementation.
|
||
|
|
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
|
||
|
|
index 17e7bb1..d45b8ff 100644
|
||
|
|
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
|
||
|
|
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
|
||
|
|
@@ -65,8 +65,11 @@ import org.eclipse.jetty.util.log.Logger;
|
||
|
|
public class MultiPartInputStreamParser
|
||
|
|
{
|
||
|
|
private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.class);
|
||
|
|
+ private static final int DEFAULT_MAX_FORM_KEYS = 1000;
|
||
|
|
public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
|
||
|
|
- public static final MultiMap<Part> EMPTY_MAP = new MultiMap(Collections.emptyMap());
|
||
|
|
+ public static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
|
||
|
|
+ private final int _maxParts;
|
||
|
|
+ private int _numParts;
|
||
|
|
protected InputStream _in;
|
||
|
|
protected MultipartConfigElement _config;
|
||
|
|
protected String _contentType;
|
||
|
|
@@ -411,9 +414,23 @@ public class MultiPartInputStreamParser
|
||
|
|
*/
|
||
|
|
public MultiPartInputStreamParser (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
|
||
|
|
{
|
||
|
|
+ this(in, contentType, config, contextTmpDir, DEFAULT_MAX_FORM_KEYS);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ /**
|
||
|
|
+ * @param in Request input stream
|
||
|
|
+ * @param contentType Content-Type header
|
||
|
|
+ * @param config MultipartConfigElement
|
||
|
|
+ * @param contextTmpDir javax.servlet.context.tempdir
|
||
|
|
+ * @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
|
||
|
|
+ */
|
||
|
|
+ public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
|
||
|
|
+
|
||
|
|
+ {
|
||
|
|
_contentType = contentType;
|
||
|
|
_config = config;
|
||
|
|
_contextTmpDir = contextTmpDir;
|
||
|
|
+ _maxParts = maxParts;
|
||
|
|
if (_contextTmpDir == null)
|
||
|
|
_contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
|
||
|
|
|
||
|
|
@@ -712,6 +729,11 @@ public class MultiPartInputStreamParser
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ // Check if we can create a new part.
|
||
|
|
+ _numParts++;
|
||
|
|
+ if (_maxParts >= 0 && _numParts > _maxParts)
|
||
|
|
+ throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
|
||
|
|
+
|
||
|
|
//Have a new Part
|
||
|
|
MultiPart part = new MultiPart(name, filename);
|
||
|
|
part.setHeaders(headers);
|