Fix #9334 Cookie Compliance (#9402)

Fix incorrect change to RFC6265 to not support dollars in cookie names.

Signed-off-by: gregw <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2023-02-21 21:31:43 +11:00 committed by GitHub
parent f01d53895f
commit 4d146412c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 525 additions and 91 deletions

View File

@ -128,13 +128,12 @@ public class CookieCompliance implements ComplianceViolation.Mode
* <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance, * <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance,
* but allows:</p> * but allows:</p>
* <ul> * <ul>
* <li>{@link Violation#ATTRIBUTES}</li>
* <li>{@link Violation#INVALID_COOKIES}</li> * <li>{@link Violation#INVALID_COOKIES}</li>
* <li>{@link Violation#OPTIONAL_WHITE_SPACE}</li> * <li>{@link Violation#OPTIONAL_WHITE_SPACE}</li>
* </ul> * </ul>
*/ */
public static final CookieCompliance RFC6265 = new CookieCompliance("RFC6265", of( public static final CookieCompliance RFC6265 = new CookieCompliance("RFC6265", of(
Violation.ATTRIBUTES, Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE) Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE)
); );
/** /**
@ -146,6 +145,7 @@ public class CookieCompliance implements ComplianceViolation.Mode
* <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance, * <p>A CookieCompliance mode that enforces <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a> compliance,
* but allows:</p> * but allows:</p>
* <ul> * <ul>
* <li>{@link Violation#ATTRIBUTES}</li>
* <li>{@link Violation#BAD_QUOTES}</li> * <li>{@link Violation#BAD_QUOTES}</li>
* <li>{@link Violation#ESCAPE_IN_QUOTES}</li> * <li>{@link Violation#ESCAPE_IN_QUOTES}</li>
* <li>{@link Violation#INVALID_COOKIES}</li> * <li>{@link Violation#INVALID_COOKIES}</li>
@ -153,8 +153,8 @@ public class CookieCompliance implements ComplianceViolation.Mode
* <li>{@link Violation#SPECIAL_CHARS_IN_QUOTES}</li> * <li>{@link Violation#SPECIAL_CHARS_IN_QUOTES}</li>
* </ul> * </ul>
*/ */
public static final CookieCompliance RFC6265_LEGACY = new CookieCompliance("RFC6265_LEGACY", of( public static final CookieCompliance RFC6265_LEGACY = new CookieCompliance("RFC6265_LEGACY", EnumSet.of(
Violation.BAD_QUOTES, Violation.ESCAPE_IN_QUOTES, Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE, Violation.SPECIAL_CHARS_IN_QUOTES) Violation.ATTRIBUTES, Violation.BAD_QUOTES, Violation.ESCAPE_IN_QUOTES, Violation.INVALID_COOKIES, Violation.OPTIONAL_WHITE_SPACE, Violation.SPECIAL_CHARS_IN_QUOTES)
); );
/** /**
@ -214,40 +214,45 @@ public class CookieCompliance implements ComplianceViolation.Mode
*/ */
public static CookieCompliance from(String spec) public static CookieCompliance from(String spec)
{ {
Set<Violation> violations; CookieCompliance compliance = valueOf(spec);
String[] elements = spec.split("\\s*,\\s*"); if (compliance == null)
switch (elements[0])
{ {
case "0": String[] elements = spec.split("\\s*,\\s*");
violations = noneOf(Violation.class); Set<Violation> violations;
break; switch (elements[0])
case "*":
violations = allOf(Violation.class);
break;
default:
{ {
CookieCompliance mode = valueOf(elements[0]); case "0" :
violations = (mode == null) ? noneOf(Violation.class) : copyOf(mode.getAllowed()); violations = noneOf(Violation.class);
break; break;
case "*" :
violations = allOf(Violation.class);
break;
default :
{
CookieCompliance mode = valueOf(elements[0]);
if (mode == null)
throw new IllegalArgumentException("Unknown base mode: " + elements[0]);
violations = (mode.getAllowed().isEmpty()) ? noneOf(Violation.class) : copyOf(mode.getAllowed());
}
} }
}
for (int i = 1; i < elements.length; i++) for (int i = 1; i < elements.length; i++)
{ {
String element = elements[i]; String element = elements[i];
boolean exclude = element.startsWith("-"); boolean exclude = element.startsWith("-");
if (exclude) if (exclude)
element = element.substring(1); element = element.substring(1);
Violation section = Violation.valueOf(element); Violation section = Violation.valueOf(element);
if (exclude) if (exclude)
violations.remove(section); violations.remove(section);
else else
violations.add(section); violations.add(section);
} }
CookieCompliance compliance = new CookieCompliance("CUSTOM" + __custom.getAndIncrement(), violations); compliance = new CookieCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
}
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("CookieCompliance from {}->{}", spec, compliance); LOG.debug("CookieCompliance from {}->{}", spec, compliance);
return compliance; return compliance;
@ -290,4 +295,10 @@ public class CookieCompliance implements ComplianceViolation.Mode
{ {
return this == mode || getAllowed().containsAll(mode.getAllowed()); return this == mode || getAllowed().containsAll(mode.getAllowed());
} }
@Override
public String toString()
{
return String.format("%s@%x%s", _name, hashCode(), _violations);
}
} }

View File

@ -41,9 +41,9 @@ public interface CookieParser
return new RFC6265CookieParser(handler, compliance, complianceListener); return new RFC6265CookieParser(handler, compliance, complianceListener);
} }
void parseField(String field); void parseField(String field) throws InvalidCookieException;
default void parseFields(List<String> rawFields) default void parseFields(List<String> rawFields) throws InvalidCookieException
{ {
// For each cookie field // For each cookie field
for (String field : rawFields) for (String field : rawFields)
@ -57,4 +57,30 @@ public interface CookieParser
{ {
void addCookie(String name, String value, int version, String domain, String path, String comment); void addCookie(String name, String value, int version, String domain, String path, String comment);
} }
/**
* <p>The exception thrown when a cookie cannot be parsed and {@link CookieCompliance.Violation#INVALID_COOKIES} is not allowed.</p>
*/
class InvalidCookieException extends IllegalArgumentException
{
public InvalidCookieException()
{
super();
}
public InvalidCookieException(String s)
{
super(s);
}
public InvalidCookieException(String message, Throwable cause)
{
super(message, cause);
}
public InvalidCookieException(Throwable cause)
{
super(cause);
}
}
} }

View File

@ -281,7 +281,7 @@ public class HttpCookie
public String getSetCookie(CookieCompliance compliance) public String getSetCookie(CookieCompliance compliance)
{ {
if (CookieCompliance.RFC6265.compliesWith(compliance)) if (compliance == null || CookieCompliance.RFC6265_LEGACY.compliesWith(compliance))
return getRFC6265SetCookie(); return getRFC6265SetCookie();
return getRFC2965SetCookie(); return getRFC2965SetCookie();
} }

View File

@ -85,7 +85,7 @@ public class RFC6265CookieParser implements CookieParser
if (token == null) if (token == null)
{ {
if (!_complianceMode.allows(INVALID_COOKIES)) if (!_complianceMode.allows(INVALID_COOKIES))
throw new IllegalArgumentException("Invalid Cookie character"); throw new InvalidCookieException("Invalid Cookie character");
state = State.INVALID_COOKIE; state = State.INVALID_COOKIE;
continue; continue;
} }
@ -100,7 +100,7 @@ public class RFC6265CookieParser implements CookieParser
if (token.isRfc2616Token()) if (token.isRfc2616Token())
{ {
if (!StringUtil.isBlank(cookieName) && c != '$') if (!StringUtil.isBlank(cookieName) && !(c == '$' && (_complianceMode.allows(ATTRIBUTES) || _complianceMode.allows(ATTRIBUTE_VALUES))))
{ {
_handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment); _handler.addCookie(cookieName, cookieValue, cookieVersion, cookieDomain, cookiePath, cookieComment);
cookieName = null; cookieName = null;
@ -120,7 +120,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie name"); throw new InvalidCookieException("Bad Cookie name");
} }
break; break;
@ -158,7 +158,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie name"); throw new InvalidCookieException("Bad Cookie name");
} }
break; break;
@ -181,7 +181,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie"); throw new InvalidCookieException("Bad Cookie");
} }
break; break;
@ -215,7 +215,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie value"); throw new InvalidCookieException("Bad Cookie value");
} }
break; break;
@ -237,7 +237,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie value"); throw new InvalidCookieException("Bad Cookie value");
} }
break; break;
@ -277,7 +277,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie quoted value"); throw new InvalidCookieException("Bad Cookie quoted value");
} }
break; break;
@ -299,7 +299,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalArgumentException("Bad Cookie quoted value"); throw new InvalidCookieException("Bad Cookie quoted value");
} }
break; break;
@ -323,7 +323,7 @@ public class RFC6265CookieParser implements CookieParser
} }
else else
{ {
throw new IllegalStateException("Comma cookie separator"); throw new InvalidCookieException("Comma cookie separator");
} }
} }
else if ((c == ' ' || c == '\t') && _complianceMode.allows(OPTIONAL_WHITE_SPACE)) else if ((c == ' ' || c == '\t') && _complianceMode.allows(OPTIONAL_WHITE_SPACE))
@ -332,7 +332,6 @@ public class RFC6265CookieParser implements CookieParser
continue; continue;
} }
boolean knownAttribute = true;
if (StringUtil.isBlank(attributeName)) if (StringUtil.isBlank(attributeName))
{ {
cookieValue = value; cookieValue = value;
@ -358,7 +357,10 @@ public class RFC6265CookieParser implements CookieParser
cookieVersion = Integer.parseInt(value); cookieVersion = Integer.parseInt(value);
break; break;
default: default:
knownAttribute = false; if (!_complianceMode.allows(INVALID_COOKIES))
throw new IllegalArgumentException("Invalid Cookie attribute");
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
break; break;
} }
} }
@ -366,31 +368,17 @@ public class RFC6265CookieParser implements CookieParser
{ {
reportComplianceViolation(ATTRIBUTES, field); reportComplianceViolation(ATTRIBUTES, field);
} }
else if (_complianceMode.allows(INVALID_COOKIES))
{
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
continue;
}
else else
{ {
throw new IllegalArgumentException("Invalid Cookie with attributes"); cookieName = attributeName;
cookieValue = value;
} }
attributeName = null; attributeName = null;
} }
value = null; value = null;
if (!knownAttribute)
{
if (!_complianceMode.allows(INVALID_COOKIES))
throw new IllegalArgumentException("Invalid Cookie attribute");
reportComplianceViolation(INVALID_COOKIES, field);
state = State.INVALID_COOKIE;
continue;
}
if (state == State.END) if (state == State.END)
throw new IllegalStateException("Invalid cookie"); throw new InvalidCookieException("Invalid cookie");
break; break;
case INVALID_COOKIE: case INVALID_COOKIE:

View File

@ -0,0 +1,137 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* Tests of poor various name=value scenarios and expectations of results
* due to our efforts at being lenient with what we receive.
*/
public class RFC6265CookieParserLenientTest
{
public static Stream<Arguments> data()
{
return Stream.of(
// Simple test to verify behavior
Arguments.of("x=y", "x", "y"),
Arguments.of("key=value", "key", "value"),
// Tests that conform to RFC2109
// RFC2109 - token values
// token = 1*<any CHAR except CTLs or tspecials>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// tspecials = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
Arguments.of("abc=xyz", "abc", "xyz"),
Arguments.of("abc=!bar", "abc", "!bar"),
Arguments.of("abc=#hash", "abc", "#hash"),
Arguments.of("abc=#hash", "abc", "#hash"),
// RFC2109 - quoted-string values
// quoted-string = ( <"> *(qdtext) <"> )
// qdtext = <any TEXT except <">>
// rejected, as name cannot be DQUOTED
Arguments.of("\"a\"=bcd", null, null),
Arguments.of("\"a\"=\"b c d\"", null, null),
// lenient with spaces and EOF
Arguments.of("abc=", "abc", ""),
Arguments.of("abc= ", "abc", ""),
Arguments.of("abc= x", "abc", "x"),
Arguments.of("abc = ", "abc", ""),
Arguments.of("abc = ;", "abc", ""),
Arguments.of("abc = ; ", "abc", ""),
Arguments.of("abc = x ", "abc", "x"),
Arguments.of("abc=\"\"", "abc", ""),
Arguments.of("abc= \"\" ", "abc", ""),
Arguments.of("abc= \"x\" ", "abc", "x"),
Arguments.of("abc= \"x\" ;", "abc", "x"),
Arguments.of("abc= \"x\" ; ", "abc", "x"),
// The backslash character ("\") may be used as a single-character quoting
// mechanism only within quoted-string and comment constructs.
// quoted-pair = "\" CHAR
Arguments.of("abc=\"xyz\"", "abc", "xyz"),
Arguments.of("abc=\"!bar\"", "abc", "!bar"),
Arguments.of("abc=\"#hash\"", "abc", "#hash"),
// RFC2109 - other valid name types that conform to 'attr'/'token' syntax
Arguments.of("!f!o!o!=wat", "!f!o!o!", "wat"),
Arguments.of("__MyHost=Foo", "__MyHost", "Foo"),
Arguments.of("some-thing-else=to-parse", "some-thing-else", "to-parse"),
Arguments.of("$foo=bar", "$foo", "bar"),
// Tests that conform to RFC6265
Arguments.of("abc=foobar!", "abc", "foobar!"),
Arguments.of("abc=\"foobar!\"", "abc", "foobar!")
);
}
@ParameterizedTest
@MethodSource("data")
public void testLenientBehavior(String rawHeader, String expectedName, String expectedValue)
{
TestCutter cutter = new TestCutter();
cutter.parseField(rawHeader);
if (expectedName == null)
assertThat("Cookies.length", cutter.names.size(), is(0));
else
{
assertThat("Cookies.length", cutter.names.size(), is(1));
assertThat("Cookie.name", cutter.names.get(0), is(expectedName));
assertThat("Cookie.value", cutter.values.get(0), is(expectedValue));
}
}
static class TestCutter implements CookieParser.Handler
{
CookieParser parser;
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
protected TestCutter()
{
parser = new RFC6265CookieParser(this, CookieCompliance.RFC6265, null);
}
public void parseField(String field)
{
parser.parseField(field);
}
@Override
public void addCookie(String cookieName, String cookieValue, int cookieVersion, String cookieDomain, String cookiePath, String cookieComment)
{
names.add(cookieName);
values.add(cookieValue);
}
}
}

View File

@ -23,7 +23,6 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class RFC6265CookieParserTest public class RFC6265CookieParserTest
{ {
@ -36,7 +35,7 @@ public class RFC6265CookieParserTest
String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\""; String rawCookie = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"";
// Test with RFC 2965. // Test with RFC 2965.
TestCookieParser parser = new TestCookieParser(CookieCompliance.RFC2965_LEGACY); TestCookieParser parser = new TestCookieParser(CookieCompliance.RFC2965);
List<Cookie> cookies = parser.parseFields(rawCookie); List<Cookie> cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(1)); assertThat("Cookies.length", cookies.size(), is(1));
@ -44,18 +43,45 @@ public class RFC6265CookieParserTest
// There are 2 attributes, so 2 violations. // There are 2 attributes, so 2 violations.
assertThat(parser.violations.size(), is(2)); assertThat(parser.violations.size(), is(2));
// Same test with RFC 6265. // Same test with RFC6265.
parser = new TestCookieParser(CookieCompliance.RFC6265); parser = new TestCookieParser(CookieCompliance.RFC6265);
cookies = parser.parseFields(rawCookie); cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(3));
assertCookie("Cookies[0]", cookies.get(0), "$Version", "1", 0, null);
assertCookie("Cookies[1]", cookies.get(1), "Customer", "WILE_E_COYOTE", 0, null);
assertCookie("Cookies[2]", cookies.get(2), "$Path", "/acme", 0, null);
// There attributes are seen as just normal cookies, so no violations
assertThat(parser.violations.size(), is(0));
// Same again, but allow attributes which are ignored
parser = new TestCookieParser(CookieCompliance.from("RFC6265,ATTRIBUTES"));
cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(1)); assertThat("Cookies.length", cookies.size(), is(1));
assertCookie("Cookies[0]", cookies.get(0), "Customer", "WILE_E_COYOTE", 0, null); assertCookie("Cookies[0]", cookies.get(0), "Customer", "WILE_E_COYOTE", 0, null);
// There are 2 attributes, so 2 violations.
// There attributes are seen as just normal cookies, so no violations
assertThat(parser.violations.size(), is(2));
// Same again, but allow attributes which are not ignored
parser = new TestCookieParser(CookieCompliance.from("RFC6265,ATTRIBUTE_VALUES"));
cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(1));
assertCookie("Cookies[0]", cookies.get(0), "Customer", "WILE_E_COYOTE", 1, "/acme");
// There attributes are seen as just normal cookies, so no violations
assertThat(parser.violations.size(), is(2)); assertThat(parser.violations.size(), is(2));
// Same test with RFC 6265 strict should throw. // Same test with RFC 6265 strict should throw.
TestCookieParser strictParser = new TestCookieParser(CookieCompliance.RFC6265_STRICT); parser = new TestCookieParser(CookieCompliance.RFC6265_STRICT);
assertThrows(IllegalArgumentException.class, () -> strictParser.parseFields(rawCookie)); cookies = parser.parseFields(rawCookie);
assertThat("Cookies.length", cookies.size(), is(3));
assertCookie("Cookies[0]", cookies.get(0), "$Version", "1", 0, null);
assertCookie("Cookies[1]", cookies.get(1), "Customer", "WILE_E_COYOTE", 0, null);
assertCookie("Cookies[2]", cookies.get(2), "$Path", "/acme", 0, null);
// There attributes are seen as just normal cookies, so no violations
assertThat(parser.violations.size(), is(0));
} }
/** /**
@ -68,11 +94,29 @@ public class RFC6265CookieParserTest
"Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " + "Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\""; "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(2)); assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme"); 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[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 1, "/acme");
cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(5));
assertCookie("Cookies[0]", cookies[0], "$Version", "1", 0, null);
assertCookie("Cookies[1]", cookies[1], "Customer", "WILE_E_COYOTE", 0, null);
assertCookie("Cookies[2]", cookies[2], "$Path", "/acme", 0, null);
assertCookie("Cookies[3]", cookies[3], "Part_Number", "Rocket_Launcher_0001", 0, null);
assertCookie("Cookies[4]", cookies[4], "$Path", "/acme", 0, null);
cookies = parseCookieHeaders(CookieCompliance.from("RFC6265,ATTRIBUTES"), rawCookie);
assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 0, null);
assertCookie("Cookies[1]", cookies[1], "Part_Number", "Rocket_Launcher_0001", 0, null);
cookies = parseCookieHeaders(CookieCompliance.from("RFC6265,ATTRIBUTE_VALUES"), 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");
} }
/** /**
@ -86,7 +130,7 @@ public class RFC6265CookieParserTest
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " + "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; " +
"Shipping=\"FedEx\"; $Path=\"/acme\""; "Shipping=\"FedEx\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(3)); assertThat("Cookies.length", cookies.length, is(3));
assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme"); assertCookie("Cookies[0]", cookies[0], "Customer", "WILE_E_COYOTE", 1, "/acme");
@ -104,7 +148,7 @@ public class RFC6265CookieParserTest
"Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " + "Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " +
"Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\""; "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(2)); assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo"); assertCookie("Cookies[0]", cookies[0], "Part_Number", "Riding_Rocket_0023", 1, "/acme/ammo");
@ -121,7 +165,7 @@ public class RFC6265CookieParserTest
"session_id=\"1234\"; " + "session_id=\"1234\"; " +
"session_id=\"1111\"; $Domain=\".cracker.edu\""; "session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(2)); assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null); assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
@ -137,15 +181,25 @@ public class RFC6265CookieParserTest
String rawCookie = "$Version=\"1\"; session_id=\"1234\", " + String rawCookie = "$Version=\"1\"; session_id=\"1234\", " +
"$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\""; "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(2)); assertThat("Cookies.length", cookies.length, is(2));
assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null); assertCookie("Cookies[0]", cookies[0], "session_id", "1234", 1, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null); assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 1, null);
cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie); cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(1)); assertThat("Cookies.length", cookies.length, is(3));
assertCookie("Cookies[1]", cookies[0], "session_id", "1111", 0, null); assertCookie("Cookies[0]", cookies[0], "$Version", "1", 0, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1111", 0, null);
assertCookie("Cookies[2]", cookies[2], "$Domain", ".cracker.edu", 0, null);
cookies = parseCookieHeaders(CookieCompliance.from("RFC6265,COMMA_SEPARATOR"), rawCookie);
assertThat("Cookies.length", cookies.length, is(5));
assertCookie("Cookies[0]", cookies[0], "$Version", "1", 0, null);
assertCookie("Cookies[1]", cookies[1], "session_id", "1234", 0, null);
assertCookie("Cookies[3]", cookies[2], "$Version", "1", 0, null);
assertCookie("Cookies[3]", cookies[3], "session_id", "1111", 0, null);
assertCookie("Cookies[4]", cookies[4], "$Domain", ".cracker.edu", 0, null);
} }
/** /**
@ -201,7 +255,8 @@ public class RFC6265CookieParserTest
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
assertThat("Cookies.length", cookies.length, is(0)); assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "$key", "value", 0, null);
} }
@Test @Test
@ -236,7 +291,7 @@ public class RFC6265CookieParserTest
public void testRFC2965QuotedEscape() public void testRFC2965QuotedEscape()
{ {
String rawCookie = "A=\"double\\\"quote\""; String rawCookie = "A=\"double\\\"quote\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(1)); assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "A", "double\"quote", 0, null); assertCookie("Cookies[0]", cookies[0], "A", "double\"quote", 0, null);
@ -246,16 +301,12 @@ public class RFC6265CookieParserTest
public void testRFC2965QuotedSpecial() public void testRFC2965QuotedSpecial()
{ {
String rawCookie = "A=\", ;\""; String rawCookie = "A=\", ;\"";
Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965_LEGACY, rawCookie); Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC2965, rawCookie);
assertThat("Cookies.length", cookies.length, is(1)); assertThat("Cookies.length", cookies.length, is(1));
assertCookie("Cookies[0]", cookies[0], "A", ", ;", 0, null); assertCookie("Cookies[0]", cookies[0], "A", ", ;", 0, null);
} }
// TODO:
// $X; N=V
// $X=Y; N=V
public static List<Param> parameters() public static List<Param> parameters()
{ {
return Arrays.asList( return Arrays.asList(
@ -281,8 +332,8 @@ public class RFC6265CookieParserTest
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("@={}"), new Param("@={}"),
new Param("$X=Y; N=V", "N=V"), new Param("$X=Y; N=V", "$X=Y", "N=V"),
new Param("N=V; $X=Y", "N=V") new Param("N=V; $X=Y", "N=V", "$X=Y")
); );
} }

View File

@ -73,8 +73,8 @@
<Set name="persistentConnectionsEnabled" property="jetty.httpConfig.persistentConnectionsEnabled"/> <Set name="persistentConnectionsEnabled" property="jetty.httpConfig.persistentConnectionsEnabled"/>
<Set name="httpCompliance"><Call class="org.eclipse.jetty.http.HttpCompliance" name="from"><Arg><Property name="jetty.httpConfig.compliance" deprecated="jetty.http.compliance" default="RFC7230"/></Arg></Call></Set> <Set name="httpCompliance"><Call class="org.eclipse.jetty.http.HttpCompliance" name="from"><Arg><Property name="jetty.httpConfig.compliance" deprecated="jetty.http.compliance" default="RFC7230"/></Arg></Call></Set>
<Set name="uriCompliance"><Call class="org.eclipse.jetty.http.UriCompliance" name="from"><Arg><Property name="jetty.httpConfig.uriCompliance" default="SAFE"/></Arg></Call></Set> <Set name="uriCompliance"><Call class="org.eclipse.jetty.http.UriCompliance" name="from"><Arg><Property name="jetty.httpConfig.uriCompliance" default="SAFE"/></Arg></Call></Set>
<Set name="requestCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.requestCookieCompliance" default="RFC6265"/></Arg></Call></Set> <Set name="requestCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="from"><Arg><Property name="jetty.httpConfig.requestCookieCompliance" default="RFC6265"/></Arg></Call></Set>
<Set name="responseCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.responseCookieCompliance" default="RFC6265"/></Arg></Call></Set> <Set name="responseCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="from"><Arg><Property name="jetty.httpConfig.responseCookieCompliance" default="RFC6265"/></Arg></Call></Set>
<Set name="multiPartFormDataCompliance"><Call class="org.eclipse.jetty.server.MultiPartFormDataCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.multiPartFormDataCompliance" default="RFC7578"/></Arg></Call></Set> <Set name="multiPartFormDataCompliance"><Call class="org.eclipse.jetty.server.MultiPartFormDataCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.multiPartFormDataCompliance" default="RFC7578"/></Arg></Call></Set>
<Set name="relativeRedirectAllowed"><Property name="jetty.httpConfig.relativeRedirectAllowed" default="false"/></Set> <Set name="relativeRedirectAllowed"><Property name="jetty.httpConfig.relativeRedirectAllowed" default="false"/></Set>
<Set name="useInputDirectByteBuffers" property="jetty.httpConfig.useInputDirectByteBuffers"/> <Set name="useInputDirectByteBuffers" property="jetty.httpConfig.useInputDirectByteBuffers"/>

View File

@ -75,7 +75,7 @@ etc/jetty.xml
## URI Compliance: DEFAULT, LEGACY, RFC3986, RFC3986_UNAMBIGUOUS, UNSAFE ## URI Compliance: DEFAULT, LEGACY, RFC3986, RFC3986_UNAMBIGUOUS, UNSAFE
# jetty.httpConfig.uriCompliance=DEFAULT # jetty.httpConfig.uriCompliance=DEFAULT
## Cookie compliance mode for parsing request Cookie headers: RFC2965, RFC6265 ## Cookie compliance mode for parsing request Cookie headers: RFC6265_STRICT, RFC6265, RFC6265_LEGACY, RFC2965, RFC2965_LEGACY
# jetty.httpConfig.requestCookieCompliance=RFC6265 # jetty.httpConfig.requestCookieCompliance=RFC6265
## Cookie compliance mode for generating response Set-Cookie: RFC2965, RFC6265 ## Cookie compliance mode for generating response Set-Cookie: RFC2965, RFC6265

View File

@ -17,9 +17,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.CookieParser; import org.eclipse.jetty.http.CookieParser;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -95,7 +97,14 @@ public class Cookies implements CookieParser.Handler
if (_parsed) if (_parsed)
return _cookies; return _cookies;
_parser.parseFields(_rawFields); try
{
_parser.parseFields(_rawFields);
}
catch (CookieParser.InvalidCookieException invalidCookieException)
{
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, invalidCookieException.getMessage(), invalidCookieException);
}
_cookies = (Cookie[])_cookieList.toArray(new Cookie[_cookieList.size()]); _cookies = (Cookie[])_cookieList.toArray(new Cookie[_cookieList.size()]);
_cookieList.clear(); _cookieList.clear();
_parsed = true; _parsed = true;

View File

@ -0,0 +1,212 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.CookieParser;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.UrlEncoded;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.eclipse.jetty.http.CookieCompliance.RFC2965;
import static org.eclipse.jetty.http.CookieCompliance.RFC2965_LEGACY;
import static org.eclipse.jetty.http.CookieCompliance.RFC6265;
import static org.eclipse.jetty.http.CookieCompliance.RFC6265_LEGACY;
import static org.eclipse.jetty.http.CookieCompliance.RFC6265_STRICT;
import static org.eclipse.jetty.http.CookieCompliance.from;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
public class ServerHttpCookieTest
{
private Server _server;
private LocalConnector _connector;
private HttpConfiguration _httpConfiguration;
@BeforeEach
public void beforeEach() throws Exception
{
_server = new Server();
_connector = new LocalConnector(_server);
_server.addConnector(_connector);
_server.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
HttpConfiguration config = baseRequest.getHttpChannel().getHttpConfiguration();
baseRequest.setHandled(true);
String setCookie = baseRequest.getParameter("SetCookie");
if (setCookie != null)
{
CookieParser parser = CookieParser.newParser((name, value, version, domain, path, comment) ->
{
Cookie cookie = new Cookie(name, value);
if (version > 0)
cookie.setVersion(version);
if (domain != null)
cookie.setDomain(domain);
if (path != null)
cookie.setPath(path);
if (comment != null)
cookie.setComment(comment);
response.addCookie(cookie);
}, RFC2965, null);
parser.parseField(setCookie);
}
Cookie[] cookies = request.getCookies();
StringBuilder out = new StringBuilder();
if (cookies != null)
{
for (Cookie cookie : cookies)
{
out
.append("[")
.append(cookie.getName())
.append('=')
.append(cookie.getValue());
if (cookie.getVersion() > 0)
out.append(";Version=").append(cookie.getVersion());
if (cookie.getPath() != null)
out.append(";Path=").append(cookie.getPath());
if (cookie.getDomain() != null)
out.append(";Domain=").append(cookie.getDomain());
out.append("]\n");
}
}
response.getWriter().println(out);
}
});
_httpConfiguration = _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration();
_server.start();
}
public static Stream<Arguments> requestCases()
{
return Stream.of(
Arguments.of(RFC6265_STRICT, "Cookie: name=value", 200, "Version=", List.of("[name=value]").toArray(new String[0])),
// Attribute tests
// TODO $name attributes are ignored because servlet 5.0 Cookie class rejects them. They are not ignored in servlet 6.0
Arguments.of(RFC6265_STRICT, "Cookie: $version=1; name=value", 200, "Version=", List.of("[name=value]").toArray(new String[0])),
Arguments.of(RFC6265, "Cookie: $version=1; name=value", 200, "Version=", List.of("[name=value]").toArray(new String[0])),
Arguments.of(RFC6265, "Cookie: name=value;$path=/path", 200, "Path=", List.of("[name=value]").toArray(new String[0])),
Arguments.of(from("RFC6265,ATTRIBUTES"), "Cookie: name=value;$path=/path", 200, "/path", List.of("name=value").toArray(new String[0])),
Arguments.of(from("RFC6265_STRICT,ATTRIBUTE_VALUES"), "Cookie: name=value;$path=/path", 200, null, List.of("name=value;Path=/path").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: name=value;$path=/path", 200, null, List.of("name=value;Path=/path").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: $Version=1;name=value;$path=/path", 200, null, List.of("name=value;Version=1;Path=/path").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: $Version=1;name=value;$path=/path;$Domain=host", 200, null, List.of("name=value;Version=1;Path=/path;Domain=host").toArray(new String[0])),
// multiple cookie tests
Arguments.of(RFC6265_STRICT, "Cookie: name=value; other=extra", 200, "Version=", List.of("[name=value]", "[other=extra]").toArray(new String[0])),
Arguments.of(RFC6265_STRICT, "Cookie: name=value, other=extra", 400, null, List.of("BadMessageException", "Comma cookie separator").toArray(new String[0])),
Arguments.of(from("RFC6265_STRICT,COMMA_SEPARATOR,"), "Cookie: name=value, other=extra", 200, "Version=", List.of("[name=value]", "[other=extra]").toArray(new String[0])),
Arguments.of(RFC6265, "Cookie: name=value, other=extra", 200, "name=value", null),
Arguments.of(RFC6265_LEGACY, "Cookie: name=value, other=extra", 200, null, List.of("[name=value, other=extra]").toArray(new String[0])),
Arguments.of(RFC6265_LEGACY, "Cookie: name=value; other=extra", 200, null, List.of("[name=value]", "other=extra]").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: name=value, other=extra", 200, "Version=", List.of("[name=value]", "[other=extra]").toArray(new String[0])),
Arguments.of(RFC2965_LEGACY, "Cookie: name=value, other=extra", 200, "Version=", List.of("[name=value]", "[other=extra]").toArray(new String[0])),
// white space
Arguments.of(RFC6265_STRICT, "Cookie: name =value", 400, null, List.of("BadMessageException", "Bad Cookie name").toArray(new String[0])),
Arguments.of(from("RFC6265,OPTIONAL_WHITE_SPACE"), "Cookie: name =value", 200, null, List.of("name=value").toArray(new String[0])),
// bad characters
Arguments.of(RFC6265_STRICT, "Cookie: name=va\\ue", 400, null, List.of("BadMessageException", "Bad Cookie value").toArray(new String[0])),
Arguments.of(RFC6265_STRICT, "Cookie: name=\"value\"", 200, "Version=", List.of("[name=value]").toArray(new String[0])),
Arguments.of(RFC6265_STRICT, "Cookie: name=\"value;other=extra\"", 400, null, List.of("BadMessageException", "Bad Cookie quoted value").toArray(new String[0])),
Arguments.of(RFC6265, "Cookie: name=\"value;other=extra\"", 200, "name=value", null),
Arguments.of(RFC6265_LEGACY, "Cookie: name=\"value;other=extra\"", 200, null, List.of("[name=value;other=extra]").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: name=\"value;other=extra\"", 200, null, List.of("[name=value;other=extra]").toArray(new String[0])),
Arguments.of(RFC2965, "Cookie: name=\"value;other=extra", 200, "name=value", null),
Arguments.of(RFC2965_LEGACY, "Cookie: name=\"value;other=extra\"", 200, null, List.of("[name=value;other=extra]").toArray(new String[0])),
Arguments.of(RFC2965_LEGACY, "Cookie: name=\"value;other=extra", 200, null, List.of("[name=\"value;other=extra]").toArray(new String[0])),
// TCK check
Arguments.of(RFC6265, "Cookie: $Version=1; name1=value1; $Domain=hostname; $Path=/servlet_jsh_cookie_web", 200, null, List.of("name1=value1").toArray(new String[0]))
);
}
@ParameterizedTest
@MethodSource("requestCases")
public void testRequestCookies(CookieCompliance compliance, String cookie, int status, String unexpected, String... expectations) throws Exception
{
_httpConfiguration.setRequestCookieCompliance(compliance);
HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse("GET / HTTP/1.0\r\n" + cookie + "\r\n\r\n"));
assertThat(response.getStatus(), equalTo(status));
if (unexpected != null)
assertThat(response.getContent(), not(containsString(unexpected)));
if (expectations != null)
for (String expected : expectations)
assertThat(response.getContent(), containsString(expected));
}
public static Stream<Arguments> responseCases()
{
return Stream.of(
Arguments.of(RFC6265_STRICT, "name=value", "name=value"),
Arguments.of(RFC6265, "name=value", "name=value"),
Arguments.of(RFC6265_LEGACY, "name=value", "name=value"),
Arguments.of(RFC2965, "name=value", "name=value"),
Arguments.of(RFC2965_LEGACY, "name=value", "name=value"),
Arguments.of(RFC6265_STRICT, "name=value;$version=1;$path=/path;$domain=domain", "name=value; Path=/path; Domain=domain"),
Arguments.of(RFC6265, "name=value;$version=1;$path=/path;$domain=domain", "name=value; Path=/path; Domain=domain"),
Arguments.of(RFC6265_LEGACY, "name=value;$version=1;$path=/path;$domain=domain", "name=value; Path=/path; Domain=domain"),
Arguments.of(RFC2965, "name=value;$version=1;$path=/path;$domain=domain", "name=value;Version=1;Path=/path;Domain=domain"),
Arguments.of(RFC2965_LEGACY, "name=value;$version=1;$path=/path;$domain=domain", "name=value;Version=1;Path=/path;Domain=domain"),
Arguments.of(RFC6265, "name=value", "name=value")
);
}
@ParameterizedTest
@MethodSource("responseCases")
public void testResponseCookies(CookieCompliance compliance, String cookie, String expected) throws Exception
{
_httpConfiguration.setResponseCookieCompliance(compliance);
HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse("GET /?SetCookie=" + UrlEncoded.encodeString(cookie) + " HTTP/1.0\r\n\r\n"));
assertThat(response.getStatus(), equalTo(200));
String setCookie = response.get(HttpHeader.SET_COOKIE);
if (expected == null)
assertThat(setCookie, nullValue());
else
assertThat(setCookie, equalTo(expected));
}
}