Fix CVE-2023-26048,CVE-2023-26049,CVE-2023-36479,CVE-2023-40167
(cherry picked from commit 7ce772366c60df81751882f1ecef8497c77eb78f)
This commit is contained in:
parent
80b7a73ace
commit
1880572103
537
CVE-2023-26048.patch
Normal file
537
CVE-2023-26048.patch
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
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);
|
||||||
831
CVE-2023-26049.patch
Normal file
831
CVE-2023-26049.patch
Normal file
@ -0,0 +1,831 @@
|
|||||||
|
From: Markus Koschany <apo@debian.org>
|
||||||
|
Date: Tue, 26 Sep 2023 23:42:03 +0200
|
||||||
|
Subject: CVE-2023-26049
|
||||||
|
|
||||||
|
Origin: https://github.com/eclipse/jetty.project/pull/9352
|
||||||
|
---
|
||||||
|
.../org/eclipse/jetty/http/CookieCompliance.java | 2 +-
|
||||||
|
.../org/eclipse/jetty/server/CookieCutter.java | 205 ++++++++++-----
|
||||||
|
.../org/eclipse/jetty/server/CookieCutterTest.java | 280 ++++++++++++++++-----
|
||||||
|
.../java/org/eclipse/jetty/server/RequestTest.java | 2 +
|
||||||
|
4 files changed, 361 insertions(+), 128 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
|
||||||
|
index b2d339c..d514c15 100644
|
||||||
|
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
|
||||||
|
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java
|
||||||
|
@@ -22,4 +22,4 @@ package org.eclipse.jetty.http;
|
||||||
|
* The compliance for Cookie handling.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
-public enum CookieCompliance { RFC6265, RFC2965 }
|
||||||
|
+public enum CookieCompliance { RFC6265, RFC2965, RFC6265_LEGACY }
|
||||||
|
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
|
||||||
|
index 5dce1cf..e28d262 100644
|
||||||
|
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
|
||||||
|
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java
|
||||||
|
@@ -107,23 +107,24 @@ public class CookieCutter
|
||||||
|
_lastCookies=null;
|
||||||
|
_fieldList.add(_fields++,f);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
-
|
||||||
|
+
|
||||||
|
protected void parseFields()
|
||||||
|
{
|
||||||
|
- _lastCookies=null;
|
||||||
|
- _cookies=null;
|
||||||
|
-
|
||||||
|
+ _lastCookies = null;
|
||||||
|
+ _cookies = null;
|
||||||
|
+
|
||||||
|
List<Cookie> cookies = new ArrayList<>();
|
||||||
|
|
||||||
|
int version = 0;
|
||||||
|
|
||||||
|
// delete excess fields
|
||||||
|
- while (_fieldList.size()>_fields)
|
||||||
|
+ while (_fieldList.size() > _fields)
|
||||||
|
+ {
|
||||||
|
_fieldList.remove(_fields);
|
||||||
|
-
|
||||||
|
- StringBuilder unquoted=null;
|
||||||
|
-
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ StringBuilder unquoted = null;
|
||||||
|
+
|
||||||
|
// For each cookie field
|
||||||
|
for (String hdr : _fieldList)
|
||||||
|
{
|
||||||
|
@@ -132,25 +133,31 @@ public class CookieCutter
|
||||||
|
|
||||||
|
Cookie cookie = null;
|
||||||
|
|
||||||
|
- boolean invalue=false;
|
||||||
|
- boolean inQuoted=false;
|
||||||
|
- boolean quoted=false;
|
||||||
|
- boolean escaped=false;
|
||||||
|
- int tokenstart=-1;
|
||||||
|
- int tokenend=-1;
|
||||||
|
+ boolean invalue = false;
|
||||||
|
+ boolean inQuoted = false;
|
||||||
|
+ boolean quoted = false;
|
||||||
|
+ boolean escaped = false;
|
||||||
|
+ boolean reject = false;
|
||||||
|
+ int tokenstart = -1;
|
||||||
|
+ int tokenend = -1;
|
||||||
|
for (int i = 0, length = hdr.length(); i <= length; i++)
|
||||||
|
{
|
||||||
|
- char c = i==length?0:hdr.charAt(i);
|
||||||
|
-
|
||||||
|
- // System.err.printf("i=%d/%d c=%s v=%b q=%b/%b e=%b u=%s s=%d e=%d \t%s=%s%n" ,i,length,c==0?"|":(""+c),invalue,inQuoted,quoted,escaped,unquoted,tokenstart,tokenend,name,value);
|
||||||
|
-
|
||||||
|
+ char c = i == length ? 0 : hdr.charAt(i);
|
||||||
|
+
|
||||||
|
// Handle quoted values for name or value
|
||||||
|
if (inQuoted)
|
||||||
|
{
|
||||||
|
+ boolean eol = c == 0 && i == hdr.length();
|
||||||
|
+ if (!eol && _compliance != CookieCompliance.RFC2965 && isRFC6265RejectedCharacter(inQuoted, c))
|
||||||
|
+ {
|
||||||
|
+ reject = true;
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
- escaped=false;
|
||||||
|
- if (c>0)
|
||||||
|
+ escaped = false;
|
||||||
|
+ if (c > 0)
|
||||||
|
unquoted.append(c);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@@ -160,7 +167,7 @@ public class CookieCutter
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
@@ -175,15 +182,24 @@ public class CookieCutter
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
- // unterminated quote, let's ignore quotes
|
||||||
|
+ // unterminated quote
|
||||||
|
+ if (_compliance == CookieCompliance.RFC6265)
|
||||||
|
+ continue;
|
||||||
|
+ // let's ignore quotes
|
||||||
|
unquoted.setLength(0);
|
||||||
|
inQuoted = false;
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ case ';':
|
||||||
|
+ if (_compliance == CookieCompliance.RFC6265)
|
||||||
|
+ reject = true;
|
||||||
|
+ else
|
||||||
|
+ unquoted.append(c);
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
default:
|
||||||
|
unquoted.append(c);
|
||||||
|
- continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@@ -191,7 +207,14 @@ public class CookieCutter
|
||||||
|
// Handle name and value state machines
|
||||||
|
if (invalue)
|
||||||
|
{
|
||||||
|
- // parse the value
|
||||||
|
+ boolean eol = c == 0 && i == hdr.length();
|
||||||
|
+ if (!eol && _compliance == CookieCompliance.RFC6265 && isRFC6265RejectedCharacter(inQuoted, c))
|
||||||
|
+ {
|
||||||
|
+ reject = true;
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // parse the cookie-value
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case ' ':
|
||||||
|
@@ -199,19 +222,19 @@ public class CookieCutter
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ',':
|
||||||
|
- if (_compliance!=CookieCompliance.RFC2965)
|
||||||
|
+ if (_compliance != CookieCompliance.RFC2965)
|
||||||
|
{
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
// must have been a bad internal quote. let's fix as best we can
|
||||||
|
- unquoted.append(hdr,tokenstart,i--);
|
||||||
|
+ unquoted.append(hdr, tokenstart, i--);
|
||||||
|
inQuoted = true;
|
||||||
|
quoted = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
- if (tokenstart<0)
|
||||||
|
+ if (tokenstart < 0)
|
||||||
|
tokenstart = i;
|
||||||
|
- tokenend=i;
|
||||||
|
+ tokenend = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// fall through
|
||||||
|
@@ -226,8 +249,8 @@ public class CookieCutter
|
||||||
|
unquoted.setLength(0);
|
||||||
|
quoted = false;
|
||||||
|
}
|
||||||
|
- else if(tokenstart>=0)
|
||||||
|
- value = tokenend>=tokenstart?hdr.substring(tokenstart, tokenend+1):hdr.substring(tokenstart);
|
||||||
|
+ else if (tokenstart >= 0)
|
||||||
|
+ value = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart);
|
||||||
|
else
|
||||||
|
value = "";
|
||||||
|
|
||||||
|
@@ -235,22 +258,22 @@ public class CookieCutter
|
||||||
|
{
|
||||||
|
if (name.startsWith("$"))
|
||||||
|
{
|
||||||
|
- if (_compliance==CookieCompliance.RFC2965)
|
||||||
|
+ if (_compliance == CookieCompliance.RFC2965)
|
||||||
|
{
|
||||||
|
String lowercaseName = name.toLowerCase(Locale.ENGLISH);
|
||||||
|
- switch(lowercaseName)
|
||||||
|
+ switch (lowercaseName)
|
||||||
|
{
|
||||||
|
case "$path":
|
||||||
|
- if (cookie!=null)
|
||||||
|
+ if (cookie != null)
|
||||||
|
cookie.setPath(value);
|
||||||
|
break;
|
||||||
|
case "$domain":
|
||||||
|
- if (cookie!=null)
|
||||||
|
+ if (cookie != null)
|
||||||
|
cookie.setDomain(value);
|
||||||
|
break;
|
||||||
|
case "$port":
|
||||||
|
- if (cookie!=null)
|
||||||
|
- cookie.setComment("$port="+value);
|
||||||
|
+ if (cookie != null)
|
||||||
|
+ cookie.setComment("$port=" + value);
|
||||||
|
break;
|
||||||
|
case "$version":
|
||||||
|
version = Integer.parseInt(value);
|
||||||
|
@@ -265,7 +288,10 @@ public class CookieCutter
|
||||||
|
cookie = new Cookie(name, value);
|
||||||
|
if (version > 0)
|
||||||
|
cookie.setVersion(version);
|
||||||
|
- cookies.add(cookie);
|
||||||
|
+ if (!reject)
|
||||||
|
+ {
|
||||||
|
+ cookies.add(cookie);
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
@@ -275,46 +301,68 @@ public class CookieCutter
|
||||||
|
|
||||||
|
name = null;
|
||||||
|
tokenstart = -1;
|
||||||
|
- invalue=false;
|
||||||
|
+ invalue = false;
|
||||||
|
+ reject = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case '"':
|
||||||
|
- if (tokenstart<0)
|
||||||
|
+ if (tokenstart < 0)
|
||||||
|
{
|
||||||
|
- tokenstart=i;
|
||||||
|
- inQuoted=true;
|
||||||
|
- if (unquoted==null)
|
||||||
|
- unquoted=new StringBuilder();
|
||||||
|
+ tokenstart = i;
|
||||||
|
+ inQuoted = true;
|
||||||
|
+ if (unquoted == null)
|
||||||
|
+ unquoted = new StringBuilder();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
+ else if (_compliance == CookieCompliance.RFC6265)
|
||||||
|
+ {
|
||||||
|
+ reject = true;
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
// fall through to default case
|
||||||
|
|
||||||
|
default:
|
||||||
|
+ if (_compliance == CookieCompliance.RFC6265 && quoted)
|
||||||
|
+ {
|
||||||
|
+ reject = true;
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
// must have been a bad internal quote. let's fix as best we can
|
||||||
|
- unquoted.append(hdr,tokenstart,i--);
|
||||||
|
+ unquoted.append(hdr, tokenstart, i--);
|
||||||
|
inQuoted = true;
|
||||||
|
quoted = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
- if (tokenstart<0)
|
||||||
|
+
|
||||||
|
+ if (_compliance == CookieCompliance.RFC6265_LEGACY && isRFC6265RejectedCharacter(inQuoted, c))
|
||||||
|
+ reject = true;
|
||||||
|
+
|
||||||
|
+ if (tokenstart < 0)
|
||||||
|
tokenstart = i;
|
||||||
|
- tokenend=i;
|
||||||
|
- continue;
|
||||||
|
+ tokenend = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
- // parse the name
|
||||||
|
+ // parse the cookie-name
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case ' ':
|
||||||
|
case '\t':
|
||||||
|
continue;
|
||||||
|
|
||||||
|
+ case ';':
|
||||||
|
+ // a cookie terminated with no '=' sign.
|
||||||
|
+ tokenstart = -1;
|
||||||
|
+ invalue = false;
|
||||||
|
+ reject = false;
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
case '=':
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
@@ -322,8 +370,8 @@ public class CookieCutter
|
||||||
|
unquoted.setLength(0);
|
||||||
|
quoted = false;
|
||||||
|
}
|
||||||
|
- else if(tokenstart>=0)
|
||||||
|
- name = tokenend>=tokenstart?hdr.substring(tokenstart, tokenend+1):hdr.substring(tokenstart);
|
||||||
|
+ else if (tokenstart >= 0)
|
||||||
|
+ name = tokenend >= tokenstart ? hdr.substring(tokenstart, tokenend + 1) : hdr.substring(tokenstart);
|
||||||
|
|
||||||
|
tokenstart = -1;
|
||||||
|
invalue = true;
|
||||||
|
@@ -333,14 +381,18 @@ public class CookieCutter
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
// must have been a bad internal quote. let's fix as best we can
|
||||||
|
- unquoted.append(hdr,tokenstart,i--);
|
||||||
|
+ unquoted.append(hdr, tokenstart, i--);
|
||||||
|
inQuoted = true;
|
||||||
|
quoted = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
- if (tokenstart<0)
|
||||||
|
- tokenstart=i;
|
||||||
|
- tokenend=i;
|
||||||
|
+
|
||||||
|
+ if (_compliance != CookieCompliance.RFC2965 && isRFC6265RejectedCharacter(inQuoted, c))
|
||||||
|
+ reject = true;
|
||||||
|
+
|
||||||
|
+ if (tokenstart < 0)
|
||||||
|
+ tokenstart = i;
|
||||||
|
+ tokenend = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -348,8 +400,45 @@ public class CookieCutter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- _cookies = (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
|
||||||
|
- _lastCookies=_cookies;
|
||||||
|
+ _cookies = cookies.toArray(new Cookie[0]);
|
||||||
|
+ _lastCookies = _cookies;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+ protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c)
|
||||||
|
+ {
|
||||||
|
+ // LEGACY test
|
||||||
|
+ if (_compliance == CookieCompliance.RFC6265_LEGACY)
|
||||||
|
+ {
|
||||||
|
+ if (inQuoted)
|
||||||
|
+ {
|
||||||
|
+ // We only reject if a Control Character is encountered
|
||||||
|
+ if (Character.isISOControl(c))
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+ else
|
||||||
|
+ {
|
||||||
|
+ return Character.isISOControl(c) || // control characters
|
||||||
|
+ c > 127 || // 8-bit characters
|
||||||
|
+ c == ',' || // comma
|
||||||
|
+ c == ';'; // semicolon
|
||||||
|
+ }
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /* From RFC6265 - Section 4.1.1 - Syntax
|
||||||
|
+ * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||||
|
+ * ; US-ASCII characters excluding CTLs,
|
||||||
|
+ * ; whitespace DQUOTE, comma, semicolon,
|
||||||
|
+ * ; and backslash
|
||||||
|
+ *
|
||||||
|
+ * Note: DQUOTE and semicolon are used as separator by the parser,
|
||||||
|
+ * so we can consider them authorized.
|
||||||
|
+ */
|
||||||
|
+ return c > 127 || // 8-bit characters
|
||||||
|
+ Character.isISOControl(c) || // control characters
|
||||||
|
+ c == ',' || // comma
|
||||||
|
+ c == '\\'; // backslash
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
|
||||||
|
index ec534a1..3e84ce6 100644
|
||||||
|
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
|
||||||
|
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/CookieCutterTest.java
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
-// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
|
||||||
|
+// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// All rights reserved. This program and the accompanying materials
|
||||||
|
// are made available under the terms of the Eclipse Public License v1.0
|
||||||
|
@@ -18,18 +18,21 @@
|
||||||
|
|
||||||
|
package org.eclipse.jetty.server;
|
||||||
|
|
||||||
|
-import static org.hamcrest.Matchers.is;
|
||||||
|
-import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
-
|
||||||
|
+import java.util.Arrays;
|
||||||
|
+import java.util.List;
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.CookieCompliance;
|
||||||
|
-import org.junit.jupiter.api.Disabled;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
+import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
+import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
+
|
||||||
|
+import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
+import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
public class CookieCutterTest
|
||||||
|
{
|
||||||
|
- private Cookie[] parseCookieHeaders(CookieCompliance compliance,String... headers)
|
||||||
|
+ private Cookie[] parseCookieHeaders(CookieCompliance compliance, String... headers)
|
||||||
|
{
|
||||||
|
CookieCutter cutter = new CookieCutter(compliance);
|
||||||
|
for (String header : headers)
|
||||||
|
@@ -38,7 +41,7 @@ public class CookieCutterTest
|
||||||
|
}
|
||||||
|
return cutter.getCookies();
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
private void assertCookie(String prefix, Cookie cookie,
|
||||||
|
String expectedName,
|
||||||
|
String expectedValue,
|
||||||
|
@@ -50,142 +53,174 @@ public class CookieCutterTest
|
||||||
|
assertThat(prefix + ".version", cookie.getVersion(), is(expectedVersion));
|
||||||
|
assertThat(prefix + ".path", cookie.getPath(), is(expectedPath));
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2109 and RFC2965
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC_Single()
|
||||||
|
+ public void testRFCSingle()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(1));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ /**
|
||||||
|
+ * Example from RFC2109 and RFC2965.
|
||||||
|
+ * <p>
|
||||||
|
+ * Lenient parsing, input has no spaces after ';' token.
|
||||||
|
+ * </p>
|
||||||
|
+ */
|
||||||
|
+ @Test
|
||||||
|
+ public void testRFCSingleLenientNoSpaces()
|
||||||
|
+ {
|
||||||
|
+ String rawCookie = "$Version=\"1\";Customer=\"WILE_E_COYOTE\";$Path=\"/acme\"";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
+ assertThat("Cookies.length", cookies.length, is(1));
|
||||||
|
+ assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2109 and RFC2965
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC_Double()
|
||||||
|
+ public void testRFCDouble()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; " +
|
||||||
|
- "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
|
||||||
|
- "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
-
|
||||||
|
+ "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
|
||||||
|
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2109 and RFC2965
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC_Triple()
|
||||||
|
+ public void testRFCTriple()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; " +
|
||||||
|
- "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
|
||||||
|
- "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
|
||||||
|
- "Shipping=\"FedEx\"; $Path=\"/acme\"";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
-
|
||||||
|
+ "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
|
||||||
|
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
|
||||||
|
+ "Shipping=\"FedEx\"; $Path=\"/acme\"";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(3));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
|
||||||
|
assertCookie("Cookies[2]", cookies[2], "Shipping", "FedEx", 1, "/acme");
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2109 and RFC2965
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC_PathExample()
|
||||||
|
+ public void testRFCPathExample()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; " +
|
||||||
|
- "Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
|
||||||
|
- "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
-
|
||||||
|
+ "Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
|
||||||
|
+ "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo");
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2109
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC2109_CookieSpoofingExample()
|
||||||
|
+ public void testRFC2109CookieSpoofingExample()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; " +
|
||||||
|
- "session_id=\"1234\"; " +
|
||||||
|
- "session_id=\"1111\"; $Domain=\".cracker.edu\"";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
-
|
||||||
|
+ "session_id=\"1234\"; " +
|
||||||
|
+ "session_id=\"1111\"; $Domain=\".cracker.edu\"";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC2965
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC2965_CookieSpoofingExample()
|
||||||
|
+ public void testRFC2965CookieSpoofingExample()
|
||||||
|
{
|
||||||
|
String rawCookie = "$Version=\"1\"; session_id=\"1234\", " +
|
||||||
|
- "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
|
||||||
|
-
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC2965,rawCookie);
|
||||||
|
+ "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
|
||||||
|
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
|
||||||
|
assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
|
||||||
|
|
||||||
|
- cookies = parseCookieHeaders(CookieCompliance.RFC6265,rawCookie);
|
||||||
|
- assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
- assertCookie("Cookies[0]", cookies[0], "session_id", "1234\", $Version=\"1", 0, null);
|
||||||
|
- assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 0, null);
|
||||||
|
+ cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+ assertThat("Cookies.length", cookies.length, is(1));
|
||||||
|
+ assertCookie("Cookies[0]", cookies[0], "session_id", "1111", 0, null);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC6265
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC6265_SidExample()
|
||||||
|
+ public void testRFC6265SidExample()
|
||||||
|
{
|
||||||
|
String rawCookie = "SID=31d4d96e407aad42";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC6265,rawCookie);
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(1));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Example from RFC6265
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
- public void testRFC6265_SidLangExample()
|
||||||
|
+ public void testRFC6265SidLangExample()
|
||||||
|
{
|
||||||
|
String rawCookie = "SID=31d4d96e407aad42; lang=en-US";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC6265,rawCookie);
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
+ assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
+ assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
|
||||||
|
+ assertCookie("Cookies[1]", cookies[1], "lang", "en-US", 0, null);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /**
|
||||||
|
+ * Example from RFC6265.
|
||||||
|
+ * <p>
|
||||||
|
+ * Lenient parsing, input has no spaces after ';' token.
|
||||||
|
+ * </p>
|
||||||
|
+ */
|
||||||
|
+ @Test
|
||||||
|
+ public void testRFC6265SidLangExampleLenient()
|
||||||
|
+ {
|
||||||
|
+ String rawCookie = "SID=31d4d96e407aad42;lang=en-US";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "SID", "31d4d96e407aad42", 0, null);
|
||||||
|
assertCookie("Cookies[1]", cookies[1], "lang", "en-US", 0, null);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Basic name=value, following RFC6265 rules
|
||||||
|
*/
|
||||||
|
@@ -193,13 +228,13 @@ public class CookieCutterTest
|
||||||
|
public void testKeyValue()
|
||||||
|
{
|
||||||
|
String rawCookie = "key=value";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC6265,rawCookie);
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(1));
|
||||||
|
assertCookie("Cookies[0]", cookies[0], "key", "value", 0, null);
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Basic name=value, following RFC6265 rules
|
||||||
|
*/
|
||||||
|
@@ -207,9 +242,116 @@ public class CookieCutterTest
|
||||||
|
public void testDollarName()
|
||||||
|
{
|
||||||
|
String rawCookie = "$key=value";
|
||||||
|
-
|
||||||
|
- Cookie cookies[] = parseCookieHeaders(CookieCompliance.RFC6265,rawCookie);
|
||||||
|
-
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
assertThat("Cookies.length", cookies.length, is(0));
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ @Test
|
||||||
|
+ public void testMultipleCookies()
|
||||||
|
+ {
|
||||||
|
+ String rawCookie = "testcookie; server.id=abcd; server.detail=cfg";
|
||||||
|
+
|
||||||
|
+ // The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign.
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
+ assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
+ assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null);
|
||||||
|
+ assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ @Test
|
||||||
|
+ public void testExcessiveSemicolons()
|
||||||
|
+ {
|
||||||
|
+ char[] excessive = new char[65535];
|
||||||
|
+ Arrays.fill(excessive, ';');
|
||||||
|
+ String rawCookie = "foo=bar; " + new String(excessive) + "; xyz=pdq";
|
||||||
|
+
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
|
||||||
|
+
|
||||||
|
+ assertThat("Cookies.length", cookies.length, is(2));
|
||||||
|
+ assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
|
||||||
|
+ assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ @ParameterizedTest
|
||||||
|
+ @MethodSource("rfc6265Cookies")
|
||||||
|
+ public void testRFC6265CookieParsing(Param param)
|
||||||
|
+ {
|
||||||
|
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, param.input);
|
||||||
|
+
|
||||||
|
+ assertThat("Cookies.length (" + dump(cookies) + ")", cookies.length, is(param.expected.size()));
|
||||||
|
+ for (int i = 0; i < cookies.length; i++)
|
||||||
|
+ {
|
||||||
|
+ Cookie cookie = cookies[i];
|
||||||
|
+ assertThat("Cookies[" + i + "] (" + dump(cookies) + ")", cookie.getName() + "=" + cookie.getValue(), is(param.expected.get(i)));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ public static List<Param> rfc6265Cookies()
|
||||||
|
+ {
|
||||||
|
+ return Arrays.asList(
|
||||||
|
+ new Param("A=1; B=2; C=3", "A=1", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1\"; B=2; C=3", "A=1", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1\"; B=\"2\"; C=\"3\"", "A=1", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1; B=2; C=\"3", "A=1", "B=2"),
|
||||||
|
+ new Param("A=1 ; B=2; C=3", "A=1", "B=2", "C=3"),
|
||||||
|
+ new Param("A= 1; B=2; C=3", "A=1", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1; B=2\"; C=3", "C=3"),
|
||||||
|
+ new Param("A=\"1; B=2; C=3"),
|
||||||
|
+ new Param("A=\"1 B=2\"; C=3", "A=1 B=2", "C=3"),
|
||||||
|
+ new Param("A=\"\"1; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"\" ; B=2; C=3", "A=", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"\"; B=2; C=3", "A=", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1\"\"; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1\"; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1\"1; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\" 1\"; B=2; C=3", "A= 1", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1 \"; B=2; C=3", "A=1 ", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\" 1 \"; B=2; C=3", "A= 1 ", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\" 1 1 \"; B=2; C=3", "A= 1 1 ", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1,; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1,\"; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\\1; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"\\1\"; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=1\u0007; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("A=\"1\u0007\"; B=2; C=3", "B=2", "C=3"),
|
||||||
|
+ new Param("€"),
|
||||||
|
+ new Param("@={}"),
|
||||||
|
+ new Param("$X=Y; N=V", "N=V"),
|
||||||
|
+ new Param("N=V; $X=Y", "N=V")
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ private static String dump(Cookie[] cookies)
|
||||||
|
+ {
|
||||||
|
+ StringBuilder sb = new StringBuilder();
|
||||||
|
+ for (Cookie cookie : cookies)
|
||||||
|
+ {
|
||||||
|
+ sb.append("<").append(cookie.getName()).append(">=<").append(cookie.getValue()).append("> | ");
|
||||||
|
+ }
|
||||||
|
+ if (sb.length() > 0)
|
||||||
|
+ sb.delete(sb.length() - 2, sb.length() - 1);
|
||||||
|
+ return sb.toString();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ private static class Param
|
||||||
|
+ {
|
||||||
|
+ private final String input;
|
||||||
|
+ private final List<String> expected;
|
||||||
|
+
|
||||||
|
+ public Param(String input, String... expected)
|
||||||
|
+ {
|
||||||
|
+ this.input = input;
|
||||||
|
+ this.expected = Arrays.asList(expected);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ @Override
|
||||||
|
+ public String toString()
|
||||||
|
+ {
|
||||||
|
+ return input + " -> " + expected.toString();
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
|
||||||
|
index 425a9ae..f119864 100644
|
||||||
|
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
|
||||||
|
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
|
||||||
|
@@ -61,6 +61,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.Part;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.BadMessageException;
|
||||||
|
+import org.eclipse.jetty.http.CookieCompliance;
|
||||||
|
import org.eclipse.jetty.http.HttpCompliance;
|
||||||
|
import org.eclipse.jetty.http.HttpTester;
|
||||||
|
import org.eclipse.jetty.http.MimeTypes;
|
||||||
|
@@ -97,6 +98,7 @@ public class RequestTest
|
||||||
|
http.getHttpConfiguration().setRequestHeaderSize(512);
|
||||||
|
http.getHttpConfiguration().setResponseHeaderSize(512);
|
||||||
|
http.getHttpConfiguration().setOutputBufferSize(2048);
|
||||||
|
+ http.getHttpConfiguration().setRequestCookieCompliance(CookieCompliance.RFC6265_LEGACY);
|
||||||
|
http.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
|
||||||
|
_connector = new LocalConnector(_server,http);
|
||||||
|
_server.addConnector(_connector);
|
||||||
50
CVE-2023-36479.patch
Normal file
50
CVE-2023-36479.patch
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
From: Markus Koschany <apo@debian.org>
|
||||||
|
Date: Wed, 27 Sep 2023 14:25:09 +0200
|
||||||
|
Subject: CVE-2023-36479
|
||||||
|
|
||||||
|
The org.eclipse.jetty.servlets.CGI Servlet should not be used anymore.
|
||||||
|
Upstream recommends to use Fast CGI instead.
|
||||||
|
|
||||||
|
Origin: https://github.com/eclipse/jetty.project/pull/9888
|
||||||
|
---
|
||||||
|
.../src/main/java/org/eclipse/jetty/servlets/CGI.java | 3 +++
|
||||||
|
.../test-jetty-webapp/src/main/webapp/WEB-INF/web.xml | 11 -----------
|
||||||
|
2 files changed, 3 insertions(+), 11 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
|
||||||
|
index 6322290..55d8f9a 100644
|
||||||
|
--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
|
||||||
|
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
|
||||||
|
@@ -67,7 +67,10 @@ import org.eclipse.jetty.util.log.Logger;
|
||||||
|
* <dt>ignoreExitState</dt>
|
||||||
|
* <dd>If true then do not act on a non-zero exec exit status")</dd>
|
||||||
|
* </dl>
|
||||||
|
+ *
|
||||||
|
+ * @deprecated do not use, no replacement, will be removed in a future release.
|
||||||
|
*/
|
||||||
|
+@Deprecated
|
||||||
|
public class CGI extends HttpServlet
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = -6182088932884791074L;
|
||||||
|
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
|
||||||
|
index 507771f..978595f 100644
|
||||||
|
--- a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
|
||||||
|
+++ b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml
|
||||||
|
@@ -121,17 +121,6 @@
|
||||||
|
<url-pattern>/dispatch/*</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
|
- <servlet>
|
||||||
|
- <servlet-name>CGI</servlet-name>
|
||||||
|
- <servlet-class>org.eclipse.jetty.servlets.CGI</servlet-class>
|
||||||
|
- <load-on-startup>1</load-on-startup>
|
||||||
|
- </servlet>
|
||||||
|
-
|
||||||
|
- <servlet-mapping>
|
||||||
|
- <servlet-name>CGI</servlet-name>
|
||||||
|
- <url-pattern>/cgi-bin/*</url-pattern>
|
||||||
|
- </servlet-mapping>
|
||||||
|
-
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>Chat</servlet-name>
|
||||||
|
<servlet-class>com.acme.ChatServlet</servlet-class>
|
||||||
256
CVE-2023-40167.patch
Normal file
256
CVE-2023-40167.patch
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
From: Markus Koschany <apo@debian.org>
|
||||||
|
Date: Tue, 26 Sep 2023 21:06:42 +0200
|
||||||
|
Subject: CVE-2023-40167
|
||||||
|
|
||||||
|
Origin: https://github.com/eclipse/jetty.project/commit/e4d596eafc887bcd813ae6e28295b5ce327def47
|
||||||
|
---
|
||||||
|
.../java/org/eclipse/jetty/http/HttpParser.java | 47 +++++++-------
|
||||||
|
.../org/eclipse/jetty/http/HttpParserTest.java | 71 +++++-----------------
|
||||||
|
2 files changed, 38 insertions(+), 80 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
|
||||||
|
index 2abc4b6..c045498 100644
|
||||||
|
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
|
||||||
|
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
|
||||||
|
@@ -501,7 +501,7 @@ public class HttpParser
|
||||||
|
/* Quick lookahead for the start state looking for a request method or a HTTP version,
|
||||||
|
* otherwise skip white space until something else to parse.
|
||||||
|
*/
|
||||||
|
- private boolean quickStart(ByteBuffer buffer)
|
||||||
|
+ private void quickStart(ByteBuffer buffer)
|
||||||
|
{
|
||||||
|
if (_requestHandler!=null)
|
||||||
|
{
|
||||||
|
@@ -512,7 +512,7 @@ public class HttpParser
|
||||||
|
buffer.position(buffer.position()+_methodString.length()+1);
|
||||||
|
|
||||||
|
setState(State.SPACE1);
|
||||||
|
- return false;
|
||||||
|
+ return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_responseHandler!=null)
|
||||||
|
@@ -522,7 +522,7 @@ public class HttpParser
|
||||||
|
{
|
||||||
|
buffer.position(buffer.position()+_version.asString().length()+1);
|
||||||
|
setState(State.SPACE1);
|
||||||
|
- return false;
|
||||||
|
+ return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -543,7 +543,7 @@ public class HttpParser
|
||||||
|
_string.setLength(0);
|
||||||
|
_string.append(t.getChar());
|
||||||
|
setState(_requestHandler!=null?State.METHOD:State.RESPONSE_VERSION);
|
||||||
|
- return false;
|
||||||
|
+ return;
|
||||||
|
}
|
||||||
|
case OTEXT:
|
||||||
|
case SPACE:
|
||||||
|
@@ -561,7 +561,6 @@ public class HttpParser
|
||||||
|
throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------------- */
|
||||||
|
@@ -979,12 +978,13 @@ public class HttpParser
|
||||||
|
switch (_header)
|
||||||
|
{
|
||||||
|
case CONTENT_LENGTH:
|
||||||
|
+ long contentLength = convertContentLength(_valueString);
|
||||||
|
if (_hasContentLength)
|
||||||
|
{
|
||||||
|
if(complianceViolation(MULTIPLE_CONTENT_LENGTHS))
|
||||||
|
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description);
|
||||||
|
- if (convertContentLength(_valueString)!=_contentLength)
|
||||||
|
- throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description);
|
||||||
|
+ if (contentLength != _contentLength)
|
||||||
|
+ throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.getDescription());
|
||||||
|
}
|
||||||
|
_hasContentLength = true;
|
||||||
|
|
||||||
|
@@ -993,11 +993,8 @@ public class HttpParser
|
||||||
|
|
||||||
|
if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
|
||||||
|
{
|
||||||
|
- _contentLength=convertContentLength(_valueString);
|
||||||
|
- if (_contentLength <= 0)
|
||||||
|
- _endOfContent=EndOfContent.NO_CONTENT;
|
||||||
|
- else
|
||||||
|
- _endOfContent=EndOfContent.CONTENT_LENGTH;
|
||||||
|
+ _contentLength = contentLength;
|
||||||
|
+ _endOfContent = EndOfContent.CONTENT_LENGTH;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
@@ -1085,15 +1082,21 @@ public class HttpParser
|
||||||
|
|
||||||
|
private long convertContentLength(String valueString)
|
||||||
|
{
|
||||||
|
- try
|
||||||
|
- {
|
||||||
|
- return Long.parseLong(valueString);
|
||||||
|
- }
|
||||||
|
- catch(NumberFormatException e)
|
||||||
|
+ if (valueString == null || valueString.length() == 0)
|
||||||
|
+ throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
|
||||||
|
+
|
||||||
|
+ long value = 0;
|
||||||
|
+ int length = valueString.length();
|
||||||
|
+
|
||||||
|
+ for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
- LOG.ignore(e);
|
||||||
|
- throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Invalid Content-Length Value",e);
|
||||||
|
+ char c = valueString.charAt(i);
|
||||||
|
+ if (c < '0' || c > '9')
|
||||||
|
+ throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
|
||||||
|
+
|
||||||
|
+ value = Math.addExact(Math.multiplyExact(value, 10L), c - '0');
|
||||||
|
}
|
||||||
|
+ return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------------- */
|
||||||
|
@@ -1485,12 +1488,11 @@ public class HttpParser
|
||||||
|
_methodString=null;
|
||||||
|
_endOfContent=EndOfContent.UNKNOWN_CONTENT;
|
||||||
|
_header=null;
|
||||||
|
- if (quickStart(buffer))
|
||||||
|
- return true;
|
||||||
|
+ quickStart(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request/response line
|
||||||
|
- if (_state.ordinal()>= State.START.ordinal() && _state.ordinal()<State.HEADER.ordinal())
|
||||||
|
+ if (_state.ordinal() < State.HEADER.ordinal())
|
||||||
|
{
|
||||||
|
if (parseLine(buffer))
|
||||||
|
return true;
|
||||||
|
@@ -1994,7 +1996,6 @@ public class HttpParser
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------------- */
|
||||||
|
- @SuppressWarnings("serial")
|
||||||
|
private static class IllegalCharacterException extends BadMessageException
|
||||||
|
{
|
||||||
|
private IllegalCharacterException(State state,HttpTokens.Token token,ByteBuffer buffer)
|
||||||
|
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
|
||||||
|
index dc340c1..c1d59cd 100644
|
||||||
|
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
|
||||||
|
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
|
||||||
|
@@ -23,6 +23,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
+import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
@@ -1653,7 +1654,7 @@ public class HttpParserTest
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
- public void testUnknownReponseVersion() throws Exception
|
||||||
|
+ public void testUnknownResponseVersion()
|
||||||
|
{
|
||||||
|
ByteBuffer buffer = BufferUtil.toBuffer(
|
||||||
|
"HPPT/7.7 200 OK\r\n"
|
||||||
|
@@ -1797,65 +1798,21 @@ public class HttpParserTest
|
||||||
|
assertEquals(HttpParser.State.CLOSED, parser.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
- @Test
|
||||||
|
- public void testBadContentLength0() throws Exception
|
||||||
|
- {
|
||||||
|
- ByteBuffer buffer = BufferUtil.toBuffer(
|
||||||
|
- "GET / HTTP/1.0\r\n"
|
||||||
|
- + "Content-Length: abc\r\n"
|
||||||
|
- + "Connection: close\r\n"
|
||||||
|
- + "\r\n");
|
||||||
|
-
|
||||||
|
- HttpParser.RequestHandler handler = new Handler();
|
||||||
|
- HttpParser parser = new HttpParser(handler);
|
||||||
|
-
|
||||||
|
- parser.parseNext(buffer);
|
||||||
|
- assertEquals("GET", _methodOrVersion);
|
||||||
|
- assertEquals("Invalid Content-Length Value", _bad);
|
||||||
|
- assertFalse(buffer.hasRemaining());
|
||||||
|
- assertEquals(HttpParser.State.CLOSE, parser.getState());
|
||||||
|
- parser.atEOF();
|
||||||
|
- parser.parseNext(BufferUtil.EMPTY_BUFFER);
|
||||||
|
- assertEquals(HttpParser.State.CLOSED, parser.getState());
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- @Test
|
||||||
|
- public void testBadContentLength1() throws Exception
|
||||||
|
- {
|
||||||
|
- ByteBuffer buffer = BufferUtil.toBuffer(
|
||||||
|
- "GET / HTTP/1.0\r\n"
|
||||||
|
- + "Content-Length: 9999999999999999999999999999999999999999999999\r\n"
|
||||||
|
- + "Connection: close\r\n"
|
||||||
|
- + "\r\n");
|
||||||
|
-
|
||||||
|
- HttpParser.RequestHandler handler = new Handler();
|
||||||
|
- HttpParser parser = new HttpParser(handler);
|
||||||
|
-
|
||||||
|
- parser.parseNext(buffer);
|
||||||
|
- assertEquals("GET", _methodOrVersion);
|
||||||
|
- assertEquals("Invalid Content-Length Value", _bad);
|
||||||
|
- assertFalse(buffer.hasRemaining());
|
||||||
|
- assertEquals(HttpParser.State.CLOSE, parser.getState());
|
||||||
|
- parser.atEOF();
|
||||||
|
- parser.parseNext(BufferUtil.EMPTY_BUFFER);
|
||||||
|
- assertEquals(HttpParser.State.CLOSED, parser.getState());
|
||||||
|
- }
|
||||||
|
|
||||||
|
- @Test
|
||||||
|
- public void testBadContentLength2() throws Exception
|
||||||
|
+ public void testBadContentLengths(String contentLength)
|
||||||
|
{
|
||||||
|
ByteBuffer buffer = BufferUtil.toBuffer(
|
||||||
|
- "GET / HTTP/1.0\r\n"
|
||||||
|
- + "Content-Length: 1.5\r\n"
|
||||||
|
- + "Connection: close\r\n"
|
||||||
|
- + "\r\n");
|
||||||
|
+ "GET /test HTTP/1.1\r\n" +
|
||||||
|
+ "Host: localhost\r\n" +
|
||||||
|
+ "Content-Length: " + contentLength + "\r\n" +
|
||||||
|
+ "\r\n" +
|
||||||
|
+ "1234567890\r\n");
|
||||||
|
|
||||||
|
HttpParser.RequestHandler handler = new Handler();
|
||||||
|
- HttpParser parser = new HttpParser(handler);
|
||||||
|
+ HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
|
||||||
|
+ parseAll(parser, buffer);
|
||||||
|
|
||||||
|
- parser.parseNext(buffer);
|
||||||
|
- assertEquals("GET", _methodOrVersion);
|
||||||
|
- assertEquals("Invalid Content-Length Value", _bad);
|
||||||
|
+ assertThat(_bad, notNullValue());
|
||||||
|
assertFalse(buffer.hasRemaining());
|
||||||
|
assertEquals(HttpParser.State.CLOSE, parser.getState());
|
||||||
|
parser.atEOF();
|
||||||
|
@@ -2066,7 +2023,7 @@ public class HttpParserTest
|
||||||
|
@Test
|
||||||
|
public void testBadIPv6Host() throws Exception
|
||||||
|
{
|
||||||
|
- try(StacklessLogging s = new StacklessLogging(HttpParser.class))
|
||||||
|
+ try (StacklessLogging ignored = new StacklessLogging(HttpParser.class))
|
||||||
|
{
|
||||||
|
ByteBuffer buffer = BufferUtil.toBuffer(
|
||||||
|
"GET / HTTP/1.1\r\n"
|
||||||
|
@@ -2254,8 +2211,8 @@ public class HttpParserTest
|
||||||
|
private String _methodOrVersion;
|
||||||
|
private String _uriOrStatus;
|
||||||
|
private String _versionOrReason;
|
||||||
|
- private List<HttpField> _fields = new ArrayList<>();
|
||||||
|
- private List<HttpField> _trailers = new ArrayList<>();
|
||||||
|
+ private final List<HttpField> _fields = new ArrayList<>();
|
||||||
|
+ private final List<HttpField> _trailers = new ArrayList<>();
|
||||||
|
private String[] _hdr;
|
||||||
|
private String[] _val;
|
||||||
|
private int _headers;
|
||||||
@ -12,7 +12,7 @@
|
|||||||
%bcond_with jp_minimal
|
%bcond_with jp_minimal
|
||||||
Name: jetty
|
Name: jetty
|
||||||
Version: 9.4.16
|
Version: 9.4.16
|
||||||
Release: 6
|
Release: 7
|
||||||
Summary: Java Webserver and Servlet Container
|
Summary: Java Webserver and Servlet Container
|
||||||
License: Apache-2.0 OR EPL-1.0
|
License: Apache-2.0 OR EPL-1.0
|
||||||
URL: http://www.eclipse.org/jetty/
|
URL: http://www.eclipse.org/jetty/
|
||||||
@ -28,6 +28,10 @@ Patch3: CVE-2021-28169.patch
|
|||||||
Patch4: CVE-2021-34428.patch
|
Patch4: CVE-2021-34428.patch
|
||||||
Patch5: CVE-2022-2047.patch
|
Patch5: CVE-2022-2047.patch
|
||||||
Patch6: CVE-2022-2048.patch
|
Patch6: CVE-2022-2048.patch
|
||||||
|
Patch7: CVE-2023-26048.patch
|
||||||
|
Patch8: CVE-2023-26049.patch
|
||||||
|
Patch9: CVE-2023-36479.patch
|
||||||
|
Patch10: CVE-2023-40167.patch
|
||||||
|
|
||||||
BuildRequires: maven-local mvn(javax.servlet:javax.servlet-api) < 4.0.0
|
BuildRequires: maven-local mvn(javax.servlet:javax.servlet-api) < 4.0.0
|
||||||
BuildRequires: mvn(org.apache.felix:maven-bundle-plugin)
|
BuildRequires: mvn(org.apache.felix:maven-bundle-plugin)
|
||||||
@ -796,6 +800,9 @@ exit 0
|
|||||||
%license LICENSE NOTICE.txt LICENSE-MIT
|
%license LICENSE NOTICE.txt LICENSE-MIT
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Oct 15 2024 wangkai <13474090681@163.com> - 9.4.16-7
|
||||||
|
- Fix CVE-2023-26048,CVE-2023-26049,CVE-2023-36479,CVE-2023-40167
|
||||||
|
|
||||||
* Thu Jul 18 2024 yaoxin <yao_xin001@hoperun.com> - 9.4.16-6
|
* Thu Jul 18 2024 yaoxin <yao_xin001@hoperun.com> - 9.4.16-6
|
||||||
- License compliance rectification
|
- License compliance rectification
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user