832 lines
34 KiB
Diff
832 lines
34 KiB
Diff
|
|
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);
|