jetty/CVE-2023-26048.patch

538 lines
24 KiB
Diff
Raw Normal View History

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);