Fix CVE-2023-26048,CVE-2023-26049,CVE-2023-36479,CVE-2023-40167

(cherry picked from commit 7ce772366c60df81751882f1ecef8497c77eb78f)
This commit is contained in:
wk333 2024-10-15 19:42:24 +08:00 committed by openeuler-sync-bot
parent 80b7a73ace
commit 1880572103
5 changed files with 1682 additions and 1 deletions

537
CVE-2023-26048.patch Normal file
View 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
View 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
View 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
View 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;

View File

@ -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