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