diff --git a/Tests/LibWeb/Text/expected/cookie.txt b/Tests/LibWeb/Text/expected/cookie.txt index 08d84d2a5a4..f21cf9d5923 100644 --- a/Tests/LibWeb/Text/expected/cookie.txt +++ b/Tests/LibWeb/Text/expected/cookie.txt @@ -1,12 +1,19 @@ Basic test: "cookie=value" Multiple cookies: "cookie1=value1; cookie2=value2; cookie3=value3" +Nameless cookie: "value" +Valueless cookie: "cookie=" +Nameless and valueless cookie: "" +Invalid control character: "" +Non-ASCII domain: "" +Secure cookie prefix: "" +Host cookie prefix: "" Large value: "cookie=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" Overly large value: "" HTTP only: "" Public suffix: "" SameSite=Lax: "cookie=value" SameSite=Strict: "cookie=value" -SameSite=None: "cookie=value" +SameSite=None: "" Max-Age (before expiration): "cookie-max-age=value" Expires (before expiration): "cookie-expires=value; cookie-max-age=value" Max-Age (after expiration): "" diff --git a/Tests/LibWeb/Text/input/cookie.html b/Tests/LibWeb/Text/input/cookie.html index d5070459d63..4087d1d4c99 100644 --- a/Tests/LibWeb/Text/input/cookie.html +++ b/Tests/LibWeb/Text/input/cookie.html @@ -31,6 +31,47 @@ deleteCookie("cookie3"); }; + const namelessCookieTest = () => { + document.cookie = "=value"; + printCookies("Nameless cookie"); + + deleteCookie(""); + }; + + const valuelessCookieTest = () => { + document.cookie = "cookie="; + printCookies("Valueless cookie"); + + deleteCookie("cookie"); + }; + + const namelessAndValuelessCookieTest = () => { + document.cookie = "="; + printCookies("Nameless and valueless cookie"); + }; + + const invalidControlCharacterTest = () => { + document.cookie = "cookie=ab\ncd"; + printCookies("Invalid control character"); + }; + + const nonASCIIDomainTest = () => { + document.cookie = "cookie=value; domain=🤓"; + printCookies("Non-ASCII domain"); + }; + + const secureCookiePrefixTest = () => { + document.cookie = "__Secure-cookie=value"; + printCookies("Secure cookie prefix"); + }; + + const hostCookiePrefixTest = () => { + document.cookie = "__Host-cookie1=value"; + document.cookie = "__Host-cookie2=value; secure"; + document.cookie = "__Host-cookie3=value; secure; path=/foo"; + printCookies("Host cookie prefix"); + }; + const largeValueTest = () => { const value = "x".repeat(256); @@ -41,7 +82,7 @@ }; const overlyLargeValueTest = () => { - const value = "x".repeat(4096 - "cookie=".length + 1); + const value = "x".repeat(4096 - "cookie".length + 1); document.cookie = `cookie=${value}`; printCookies("Overly large value"); @@ -129,6 +170,15 @@ basicTest(); multipleCookiesTest(); + namelessCookieTest(); + valuelessCookieTest(); + namelessAndValuelessCookieTest(); + + invalidControlCharacterTest(); + nonASCIIDomainTest(); + secureCookiePrefixTest(); + hostCookiePrefixTest(); + largeValueTest(); overlyLargeValueTest(); diff --git a/Userland/Libraries/LibWeb/Cookie/Cookie.h b/Userland/Libraries/LibWeb/Cookie/Cookie.h index e7b4f6fd3af..706942df799 100644 --- a/Userland/Libraries/LibWeb/Cookie/Cookie.h +++ b/Userland/Libraries/LibWeb/Cookie/Cookie.h @@ -31,7 +31,7 @@ struct Cookie { String name; String value; - SameSite same_site; + SameSite same_site { SameSite::Default }; UnixDateTime creation_time {}; UnixDateTime last_access_time {}; UnixDateTime expiry_time {}; diff --git a/Userland/Libraries/LibWeb/Cookie/ParsedCookie.cpp b/Userland/Libraries/LibWeb/Cookie/ParsedCookie.cpp index 67b34d3b700..d34fcb605fc 100644 --- a/Userland/Libraries/LibWeb/Cookie/ParsedCookie.cpp +++ b/Userland/Libraries/LibWeb/Cookie/ParsedCookie.cpp @@ -17,8 +17,6 @@ namespace Web::Cookie { -static constexpr size_t s_max_cookie_size = 4096; - static void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attributes); static void process_attribute(ParsedCookie& parsed_cookie, StringView attribute_name, StringView attribute_value); static void on_expires_attribute(ParsedCookie& parsed_cookie, StringView attribute_value); @@ -30,49 +28,69 @@ static void on_http_only_attribute(ParsedCookie& parsed_cookie); static void on_same_site_attribute(ParsedCookie& parsed_cookie, StringView attribute_value); static Optional parse_date_time(StringView date_string); +bool cookie_contains_invalid_control_character(StringView cookie_string) +{ + for (auto code_point : Utf8View { cookie_string }) { + if (code_point <= 0x08) + return true; + if (code_point >= 0x0a && code_point <= 0x1f) + return true; + if (code_point == 0x7f) + return true; + } + + return false; +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-6 Optional parse_cookie(StringView cookie_string) { - // https://tools.ietf.org/html/rfc6265#section-5.2 - - if (cookie_string.length() > s_max_cookie_size) + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F character (CTL characters excluding HTAB): + // Abort these steps and ignore the set-cookie-string entirely. + if (cookie_contains_invalid_control_character(cookie_string)) return {}; StringView name_value_pair; StringView unparsed_attributes; - // 1. If the set-cookie-string contains a %x3B (";") character: + // 2. If the set-cookie-string contains a %x3B (";") character: if (auto position = cookie_string.find(';'); position.has_value()) { - // The name-value-pair string consists of the characters up to, but not including, the first %x3B (";"), and the unparsed- - // attributes consist of the remainder of the set-cookie-string (including the %x3B (";") in question). + // 1. The name-value-pair string consists of the characters up to, but not including, the first %x3B (";"), and + // the unparsed-attributes consist of the remainder of the set-cookie-string (including the %x3B (";") in + // question). name_value_pair = cookie_string.substring_view(0, position.value()); unparsed_attributes = cookie_string.substring_view(position.value()); - } else { - // The name-value-pair string consists of all the characters contained in the set-cookie-string, and the unparsed- - // attributes is the empty string. + } + // Otherwise: + else { + // 1. The name-value-pair string consists of all the characters contained in the set-cookie-string, and the + // unparsed-attributes is the empty string. name_value_pair = cookie_string; } StringView name; StringView value; - if (auto position = name_value_pair.find('='); position.has_value()) { - // 3. The (possibly empty) name string consists of the characters up to, but not including, the first %x3D ("=") character, and the - // (possibly empty) value string consists of the characters after the first %x3D ("=") character. + // 3. If the name-value-pair string lacks a %x3D ("=") character, then the name string is empty, and the value + // string is the value of name-value-pair. + if (auto position = name_value_pair.find('='); !position.has_value()) { + value = name_value_pair; + } else { + // Otherwise, the name string consists of the characters up to, but not including, the first %x3D ("=") character + // and the (possibly empty) value string consists of the characters after the first %x3D ("=") character. name = name_value_pair.substring_view(0, position.value()); if (position.value() < name_value_pair.length() - 1) value = name_value_pair.substring_view(position.value() + 1); - } else { - // 2. If the name-value-pair string lacks a %x3D ("=") character, ignore the set-cookie-string entirely. - return {}; } // 4. Remove any leading or trailing WSP characters from the name string and the value string. name = name.trim_whitespace(); value = value.trim_whitespace(); - // 5. If the name string is empty, ignore the set-cookie-string entirely. - if (name.is_empty()) + // 5. If the sum of the lengths of the name string and the value string is more than 4096 octets, abort these steps + // and ignore the set-cookie-string entirely. + if (name.length() + value.length() > 4096) return {}; // 6. The cookie-name is the name string, and the cookie-value is the value string. @@ -82,6 +100,7 @@ Optional parse_cookie(StringView cookie_string) return parsed_cookie; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-8 void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attributes) { // 1. If the unparsed-attributes string is empty, skip the rest of these steps. @@ -95,28 +114,34 @@ void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attribute // 3. If the remaining unparsed-attributes contains a %x3B (";") character: if (auto position = unparsed_attributes.find(';'); position.has_value()) { - // Consume the characters of the unparsed-attributes up to, but not including, the first %x3B (";") character. + // 1. Consume the characters of the unparsed-attributes up to, but not including, the first %x3B (";") character. cookie_av = unparsed_attributes.substring_view(0, position.value()); unparsed_attributes = unparsed_attributes.substring_view(position.value()); - } else { - // Consume the remainder of the unparsed-attributes. + } + // Otherwise: + else { + // 1. Consume the remainder of the unparsed-attributes. cookie_av = unparsed_attributes; unparsed_attributes = {}; } + // Let the cookie-av string be the characters consumed in this step. StringView attribute_name; StringView attribute_value; // 4. If the cookie-av string contains a %x3D ("=") character: if (auto position = cookie_av.find('='); position.has_value()) { - // The (possibly empty) attribute-name string consists of the characters up to, but not including, the first %x3D ("=") - // character, and the (possibly empty) attribute-value string consists of the characters after the first %x3D ("=") character. + // 1. The (possibly empty) attribute-name string consists of the characters up to, but not including, the first + // %x3D ("=") character, and the (possibly empty) attribute-value string consists of the characters after the + // first %x3D ("=") character. attribute_name = cookie_av.substring_view(0, position.value()); if (position.value() < cookie_av.length() - 1) attribute_value = cookie_av.substring_view(position.value() + 1); - } else { - // The attribute-name string consists of the entire cookie-av string, and the attribute-value string is empty. + } + // Otherwise: + else { + // 1. The attribute-name string consists of the entire cookie-av string, and the attribute-value string is empty. attribute_name = cookie_av; } @@ -124,11 +149,18 @@ void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attribute attribute_name = attribute_name.trim_whitespace(); attribute_value = attribute_value.trim_whitespace(); - // 6. Process the attribute-name and attribute-value according to the requirements in the following subsections. + // 6. If the attribute-value is longer than 1024 octets, ignore the cookie-av string and return to Step 1 of this + // algorithm. + if (attribute_value.length() > 1024) { + parse_attributes(parsed_cookie, unparsed_attributes); + return; + } + + // 7. Process the attribute-name and attribute-value according to the requirements in the following subsections. // (Notice that attributes with unrecognized attribute-names are ignored.) process_attribute(parsed_cookie, attribute_name, attribute_value); - // 7. Return to Step 1 of this algorithm. + // 8. Return to Step 1 of this algorithm. parse_attributes(parsed_cookie, unparsed_attributes); } @@ -151,92 +183,139 @@ void process_attribute(ParsedCookie& parsed_cookie, StringView attribute_name, S } } +static constexpr AK::Duration maximum_cookie_age() +{ + return AK::Duration::from_seconds(400LL * 24 * 60 * 60); +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.1 void on_expires_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { - // https://tools.ietf.org/html/rfc6265#section-5.2.1 - if (auto expiry_time = parse_date_time(attribute_value); expiry_time.has_value()) - parsed_cookie.expiry_time_from_expires_attribute = expiry_time.release_value(); -} + // 1. Let the expiry-time be the result of parsing the attribute-value as cookie-date (see Section 5.1.1). + auto expiry_time = parse_date_time(attribute_value); -void on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) -{ - // https://tools.ietf.org/html/rfc6265#section-5.2.2 - - // If the first character of the attribute-value is not a DIGIT or a "-" character, ignore the cookie-av. - if (attribute_value.is_empty() || (!isdigit(attribute_value[0]) && (attribute_value[0] != '-'))) + // 2. If the attribute-value failed to parse as a cookie date, ignore the cookie-av. + if (!expiry_time.has_value()) return; - // Let delta-seconds be the attribute-value converted to an integer. - if (auto delta_seconds = attribute_value.to_number(); delta_seconds.has_value()) { - if (*delta_seconds <= 0) { - // If delta-seconds is less than or equal to zero (0), let expiry-time be the earliest representable date and time. - parsed_cookie.expiry_time_from_max_age_attribute = UnixDateTime::earliest(); - } else { - // Otherwise, let the expiry-time be the current date and time plus delta-seconds seconds. - parsed_cookie.expiry_time_from_max_age_attribute = UnixDateTime::now() + AK::Duration::from_seconds(*delta_seconds); - } - } + // 3. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days in the future or sooner, see + // Section 5.5). + auto cookie_age_limit = UnixDateTime::now() + maximum_cookie_age(); + + // 4. If the expiry-time is more than cookie-age-limit, the user agent MUST set the expiry time to cookie-age-limit + // in seconds. + if (expiry_time->seconds_since_epoch() > cookie_age_limit.seconds_since_epoch()) + expiry_time = cookie_age_limit; + + // 5. If the expiry-time is earlier than the earliest date the user agent can represent, the user agent MAY replace + // the expiry-time with the earliest representable date. + if (auto earliest = UnixDateTime::earliest(); *expiry_time < earliest) + expiry_time = earliest; + + // 6. Append an attribute to the cookie-attribute-list with an attribute-name of Expires and an attribute-value of + // expiry-time. + parsed_cookie.expiry_time_from_expires_attribute = expiry_time.release_value(); } -void on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.2 +void on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { - // https://tools.ietf.org/html/rfc6265#section-5.2.3 - - // If the attribute-value is empty, the behavior is undefined. However, the user agent SHOULD ignore the cookie-av entirely. + // 1. If the attribute-value is empty, ignore the cookie-av. if (attribute_value.is_empty()) return; - StringView cookie_domain; - - // If the first character of the attribute-value string is %x2E ("."): - if (attribute_value[0] == '.') { - // Let cookie-domain be the attribute-value without the leading %x2E (".") character. - cookie_domain = attribute_value.substring_view(1); - } else { - // Let cookie-domain be the entire attribute-value. - cookie_domain = attribute_value; - } - - // Convert the cookie-domain to lower case. - parsed_cookie.domain = MUST(Infra::to_ascii_lowercase(cookie_domain)); -} - -void on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) -{ - // https://tools.ietf.org/html/rfc6265#section-5.2.4 - - // If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"): - if (attribute_value.is_empty() || attribute_value[0] != '/') - // Let cookie-path be the default-path. + // 2. If the first character of the attribute-value is neither a DIGIT, nor a "-" character followed by a DIGIT, + // ignore the cookie-av. + if (!is_ascii_digit(attribute_value[0]) && attribute_value[0] != '-') return; - // Let cookie-path be the attribute-value - parsed_cookie.path = MUST(String::from_utf8(attribute_value)); + // 3. If the remainder of attribute-value contains a non-DIGIT character, ignore the cookie-av. + // 4. Let delta-seconds be the attribute-value converted to a base 10 integer. + auto delta_seconds = attribute_value.to_number(); + if (!delta_seconds.has_value()) + return; + + // 5. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days or less, see Section 5.5). + auto cookie_age_limit = maximum_cookie_age(); + + // 6. Set delta-seconds to the smaller of its present value and cookie-age-limit. + if (*delta_seconds > cookie_age_limit.to_seconds()) + delta_seconds = cookie_age_limit.to_seconds(); + + // 7. If delta-seconds is less than or equal to zero (0), let expiry-time be the earliest representable date and + // time. Otherwise, let the expiry-time be the current date and time plus delta-seconds seconds. + auto expiry_time = *delta_seconds <= 0 + ? UnixDateTime::earliest() + : UnixDateTime::now() + AK::Duration::from_seconds(*delta_seconds); + + // 8. Append an attribute to the cookie-attribute-list with an attribute-name of Max-Age and an attribute-value of + // expiry-time. + parsed_cookie.expiry_time_from_max_age_attribute = expiry_time; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.3 +void on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) +{ + // 1. Let cookie-domain be the attribute-value. + auto cookie_domain = attribute_value; + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be cookie-domain without its leading %x2E ("."). + if (cookie_domain.starts_with('.')) + cookie_domain = cookie_domain.substring_view(1); + + // 3. Convert the cookie-domain to lower case. + auto lowercase_cookie_domain = MUST(Infra::to_ascii_lowercase(cookie_domain)); + + // 4. Append an attribute to the cookie-attribute-list with an attribute-name of Domain and an attribute-value of + // cookie-domain. + parsed_cookie.domain = move(lowercase_cookie_domain); +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.4 +void on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) +{ + // 1. If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"): + if (attribute_value.is_empty() || attribute_value[0] != '/') { + // Let cookie-path be the default-path. + return; + } + + // Otherwise: + // 1. Let cookie-path be the attribute-value. + auto cookie_path = attribute_value; + + // 2. Append an attribute to the cookie-attribute-list with an attribute-name of Path and an attribute-value of + // cookie-path. + parsed_cookie.path = MUST(String::from_utf8(cookie_path)); +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.5 void on_secure_attribute(ParsedCookie& parsed_cookie) { - // https://tools.ietf.org/html/rfc6265#section-5.2.5 parsed_cookie.secure_attribute_present = true; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.6 void on_http_only_attribute(ParsedCookie& parsed_cookie) { - // https://tools.ietf.org/html/rfc6265#section-5.2.6 parsed_cookie.http_only_attribute_present = true; } -// https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-samesite-attribute-2 +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.7 void on_same_site_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { - // 1. Let enforcement be "Default" - + // 1. Let enforcement be "Default". // 2. If cookie-av's attribute-value is a case-insensitive match for "None", set enforcement to "None". // 3. If cookie-av's attribute-value is a case-insensitive match for "Strict", set enforcement to "Strict". // 4. If cookie-av's attribute-value is a case-insensitive match for "Lax", set enforcement to "Lax". - parsed_cookie.same_site_attribute = same_site_from_string(attribute_value); + auto enforcement = same_site_from_string(attribute_value); + + // 5. Append an attribute to the cookie-attribute-list with an attribute-name of "SameSite" and an attribute-value + // of enforcement. + parsed_cookie.same_site_attribute = enforcement; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.1 Optional parse_date_time(StringView date_string) { // https://tools.ietf.org/html/rfc6265#section-5.1.1 @@ -302,20 +381,38 @@ Optional parse_date_time(StringView date_string) // 1. Using the grammar below, divide the cookie-date into date-tokens. Vector date_tokens = date_string.split_view_if(is_delimiter); - // 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date. + // 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date: bool found_time = false; bool found_day_of_month = false; bool found_month = false; bool found_year = false; for (auto const& date_token : date_tokens) { + // 1. If the found-time flag is not set and the token matches the time production, set the found-time flag and + // set the hour-value, minute-value, and second-value to the numbers denoted by the digits in the date-token, + // respectively. Skip the remaining sub-steps and continue to the next date-token. if (!found_time && parse_time(date_token)) { found_time = true; - } else if (!found_day_of_month && parse_day_of_month(date_token)) { + } + + // 2. If the found-day-of-month flag is not set and the date-token matches the day-of-month production, set the + // found-day-of-month flag and set the day-of-month-value to the number denoted by the date-token. Skip the + // remaining sub-steps and continue to the next date-token. + else if (!found_day_of_month && parse_day_of_month(date_token)) { found_day_of_month = true; - } else if (!found_month && parse_month(date_token)) { + } + + // 3. If the found-month flag is not set and the date-token matches the month production, set the found-month + // flag and set the month-value to the month denoted by the date-token. Skip the remaining sub-steps and + // continue to the next date-token. + else if (!found_month && parse_month(date_token)) { found_month = true; - } else if (!found_year && parse_year(date_token)) { + } + + // 4. If the found-year flag is not set and the date-token matches the year production, set the found-year flag + // and set the year-value to the number denoted by the date-token. Skip the remaining sub-steps and continue + // to the next date-token. + else if (!found_year && parse_year(date_token)) { found_year = true; } } @@ -329,16 +426,22 @@ Optional parse_date_time(StringView date_string) year += 2000; // 5. Abort these steps and fail to parse the cookie-date if: - if (!found_time || !found_day_of_month || !found_month || !found_year) + // * at least one of the found-day-of-month, found-month, found-year, or found-time flags is not set, + if (!found_day_of_month || !found_month || !found_year || !found_time) return {}; + // * the day-of-month-value is less than 1 or greater than 31, if (day_of_month < 1 || day_of_month > 31) return {}; + // * the year-value is less than 1601, if (year < 1601) return {}; + // * the hour-value is greater than 23, if (hour > 23) return {}; + // * the minute-value is greater than 59, or if (minute > 59) return {}; + // * the second-value is greater than 59. if (second > 59) return {}; diff --git a/Userland/Libraries/LibWeb/Cookie/ParsedCookie.h b/Userland/Libraries/LibWeb/Cookie/ParsedCookie.h index 3108896468f..6be5d212235 100644 --- a/Userland/Libraries/LibWeb/Cookie/ParsedCookie.h +++ b/Userland/Libraries/LibWeb/Cookie/ParsedCookie.h @@ -27,6 +27,7 @@ struct ParsedCookie { }; Optional parse_cookie(StringView cookie_string); +bool cookie_contains_invalid_control_character(StringView); } diff --git a/Userland/Libraries/LibWebView/CookieJar.cpp b/Userland/Libraries/LibWebView/CookieJar.cpp index 8fc63de8c57..21d5237a15a 100644 --- a/Userland/Libraries/LibWebView/CookieJar.cpp +++ b/Userland/Libraries/LibWebView/CookieJar.cpp @@ -86,6 +86,7 @@ CookieJar::~CookieJar() m_persisted_storage->synchronization_timer->on_timeout(); } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3 String CookieJar::get_cookie(const URL::URL& url, Web::Cookie::Source source) { m_transient_storage.purge_expired_cookies(); @@ -95,15 +96,23 @@ String CookieJar::get_cookie(const URL::URL& url, Web::Cookie::Source source) return {}; auto cookie_list = get_matching_cookies(url, domain.value(), source); + + // 4. Serialize the cookie-list into a cookie-string by processing each cookie in the cookie-list in order: StringBuilder builder; for (auto const& cookie : cookie_list) { - // If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ") if (!builder.is_empty()) builder.append("; "sv); - // Output the cookie's name, the %x3D ("=") character, and the cookie's value. - builder.appendff("{}={}", cookie.name, cookie.value); + // 1. If the cookies' name is not empty, output the cookie's name followed by the %x3D ("=") character. + if (!cookie.name.is_empty()) + builder.appendff("{}=", cookie.name); + + // 2. If the cookies' value is not empty, output the cookie's value. + if (!cookie.value.is_empty()) + builder.append(cookie.value); + + // 3. If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; "). } return MUST(builder.to_string()); @@ -118,22 +127,22 @@ void CookieJar::set_cookie(const URL::URL& url, Web::Cookie::ParsedCookie const& store_cookie(parsed_cookie, url, domain.release_value(), source); } -// This is based on https://www.rfc-editor.org/rfc/rfc6265#section-5.3 as store_cookie() below -// however the whole ParsedCookie->Cookie conversion is skipped. +// This is based on store_cookie() below, however the whole ParsedCookie->Cookie conversion is skipped. void CookieJar::update_cookie(Web::Cookie::Cookie cookie) { CookieStorageKey key { cookie.name, cookie.domain, cookie.path }; - // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: - if (auto old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value()) { - // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. + // 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the + // newly-created cookie: + if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) { + // 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie. cookie.creation_time = old_cookie->creation_time; - // Remove the old-cookie from the cookie store. + // 4. Remove the old-cookie from the cookie store. // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. } - // 12. Insert the newly created cookie into the cookie store. + // 24. Insert the newly-created cookie into the cookie store. m_transient_storage.set_cookie(move(key), move(cookie)); m_transient_storage.purge_expired_cookies(); @@ -204,59 +213,62 @@ Optional CookieJar::get_named_cookie(URL::URL const& url, S return {}; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.2 Optional CookieJar::canonicalize_domain(const URL::URL& url) { - // https://tools.ietf.org/html/rfc6265#section-5.1.2 - if (!url.is_valid()) + if (!url.is_valid() || url.host().has()) return {}; - // FIXME: Implement RFC 5890 to "Convert each label that is not a Non-Reserved LDH (NR-LDH) label to an A-label". - if (url.host().has()) - return {}; + // 1. Convert the host name to a sequence of individual domain name labels. + // 2. Convert each label that is not a Non-Reserved LDH (NR-LDH) label, to an A-label (see Section 2.3.2.1 of + // [RFC5890] for the former and latter), or to a "punycode label" (a label resulting from the "ToASCII" conversion + // in Section 4 of [RFC3490]), as appropriate (see Section 6.3 of this specification). + // 3. Concatenate the resulting labels, separated by a %x2E (".") character. + // FIXME: Implement the above conversions. return MUST(MUST(url.serialized_host()).to_lowercase()); } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.3 bool CookieJar::domain_matches(StringView string, StringView domain_string) { - // https://tools.ietf.org/html/rfc6265#section-5.1.3 - // A string domain-matches a given domain string if at least one of the following conditions hold: - // The domain string and the string are identical. + // * The domain string and the string are identical. (Note that both the domain string and the string will have been + // canonicalized to lower case at this point.) if (string == domain_string) return true; - // All of the following conditions hold: + // * All of the following conditions hold: // - The domain string is a suffix of the string. - // - The last character of the string that is not included in the domain string is a %x2E (".") character. - // - The string is a host name (i.e., not an IP address). if (!string.ends_with(domain_string)) return false; + // - The last character of the string that is not included in the domain string is a %x2E (".") character. if (string[string.length() - domain_string.length() - 1] != '.') return false; + // - The string is a host name (i.e., not an IP address). if (AK::IPv4Address::from_string(string).has_value()) return false; return true; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.4 bool CookieJar::path_matches(StringView request_path, StringView cookie_path) { - // https://tools.ietf.org/html/rfc6265#section-5.1.4 - // A request-path path-matches a given cookie-path if at least one of the following conditions holds: - // The cookie-path and the request-path are identical. + // * The cookie-path and the request-path are identical. if (request_path == cookie_path) return true; if (request_path.starts_with(cookie_path)) { - // The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/"). + // * The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/"). if (cookie_path.ends_with('/')) return true; - // The cookie-path is a prefix of the request-path, and the first character of the request-path that is not included in the cookie-path is a %x2F ("/") character. + // * The cookie-path is a prefix of the request-path, and the first character of the request-path that is not + // included in the cookie-path is a %x2F ("/") character. if (request_path[cookie_path.length()] == '/') return true; } @@ -264,14 +276,14 @@ bool CookieJar::path_matches(StringView request_path, StringView cookie_path) return false; } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.4 String CookieJar::default_path(const URL::URL& url) { - // https://tools.ietf.org/html/rfc6265#section-5.1.4 - // 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise). auto uri_path = URL::percent_decode(url.serialize_path()); - // 2. If the uri-path is empty or if the first character of the uri-path is not a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. + // 2. If the uri-path is empty or if the first character of the uri-path is not a %x2F ("/") character, output + // %x2F ("/") and skip the remaining steps. if (uri_path.is_empty() || (uri_path[0] != '/')) return "/"_string; @@ -282,138 +294,325 @@ String CookieJar::default_path(const URL::URL& url) if (last_separator == 0) return "/"_string; - // 4. Output the characters of the uri-path from the first character up to, but not including, the right-most %x2F ("/"). + // 4. Output the characters of the uri-path from the first character up to, but not including, the right-most + // %x2F ("/"). // FIXME: The path might not be valid UTF-8. return MUST(String::from_utf8(uri_path.substring_view(0, last_separator))); } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#name-storage-model void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL::URL& url, String canonicalized_domain, Web::Cookie::Source source) { - // https://tools.ietf.org/html/rfc6265#section-5.3 + // 1. A user agent MAY ignore a received cookie in its entirety. See Section 5.3. - // 2. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time to the current date and time. - Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value, parsed_cookie.same_site_attribute }; + // 2. If cookie-name is empty and cookie-value is empty, abort these steps and ignore the cookie entirely. + if (parsed_cookie.name.is_empty() && parsed_cookie.value.is_empty()) + return; + + // 3. If the cookie-name or the cookie-value contains a %x00-08 / %x0A-1F / %x7F character (CTL characters + // excluding HTAB), abort these steps and ignore the cookie entirely. + if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.name)) + return; + if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.value)) + return; + + // 4. If the sum of the lengths of cookie-name and cookie-value is more than 4096 octets, abort these steps and + // ignore the cookie entirely. + if (parsed_cookie.name.byte_count() + parsed_cookie.value.byte_count() > 4096) + return; + + // 5. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time + // to the current date and time. + Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value }; cookie.creation_time = UnixDateTime::now(); cookie.last_access_time = cookie.creation_time; + // 6. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age": if (parsed_cookie.expiry_time_from_max_age_attribute.has_value()) { - // 3. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age": Set the cookie's persistent-flag to true. - // Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Max-Age". + // 1. Set the cookie's persistent-flag to true. cookie.persistent = true; + + // 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with + // an attribute-name of "Max-Age". cookie.expiry_time = parsed_cookie.expiry_time_from_max_age_attribute.value(); - } else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) { - // If the cookie-attribute-list contains an attribute with an attribute-name of "Expires": Set the cookie's persistent-flag to true. - // Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Expires". + } + // Otherwise, if the cookie-attribute-list contains an attribute with an attribute-name of "Expires" (and does not + // contain an attribute with an attribute-name of "Max-Age"): + else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) { + // 1. Set the cookie's persistent-flag to true. cookie.persistent = true; + + // 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with + // an attribute-name of "Expires". cookie.expiry_time = parsed_cookie.expiry_time_from_expires_attribute.value(); - } else { - // Set the cookie's persistent-flag to false. Set the cookie's expiry-time to the latest representable date. + } + // Otherwise: + else { + // 1. Set the cookie's persistent-flag to false. cookie.persistent = false; + + // 2. Set the cookie's expiry-time to the latest representable date. cookie.expiry_time = UnixDateTime::from_unix_time_parts(3000, 1, 1, 0, 0, 0, 0); } - // 4. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain": + String domain_attribute; + + // 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain": if (parsed_cookie.domain.has_value()) { - // Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Domain". - cookie.domain = parsed_cookie.domain.value(); + // 1. Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with + // both an attribute-name of "Domain" and an attribute-value whose length is no more than 1024 octets. (Note + // that a leading %x2E ("."), if present, is ignored even though that character is not permitted.) + if (parsed_cookie.domain->byte_count() <= 1024) + domain_attribute = parsed_cookie.domain.value(); + } + // Otherwise: + else { + // 1. Let the domain-attribute be the empty string. } - // 5. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix: - if (is_public_suffix(cookie.domain)) { - // If the domain-attribute is identical to the canonicalized request-host: - if (cookie.domain == canonicalized_domain) { - // Let the domain-attribute be the empty string. - cookie.domain = String {}; + // 8. If the domain-attribute contains a character that is not in the range of [USASCII] characters, abort these + // steps and ignore the cookie entirely. + for (auto code_point : domain_attribute.code_points()) { + if (!is_ascii(code_point)) + return; + } + + // 9. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix: + if (is_public_suffix(domain_attribute)) { + // 1. If the domain-attribute is identical to the canonicalized request-host: + if (domain_attribute == canonicalized_domain) { + // 1. Let the domain-attribute be the empty string. + domain_attribute = String {}; } // Otherwise: else { - // Ignore the cookie entirely and abort these steps. + // 1. Abort these steps and ignore the cookie entirely. return; } } - // 6. If the domain-attribute is non-empty: - if (!cookie.domain.is_empty()) { - // If the canonicalized request-host does not domain-match the domain-attribute: Ignore the cookie entirely and abort these steps. - if (!domain_matches(canonicalized_domain, cookie.domain)) + // 10. If the domain-attribute is non-empty: + if (!domain_attribute.is_empty()) { + // 1. If the canonicalized request-host does not domain-match the domain-attribute: + if (!domain_matches(canonicalized_domain, domain_attribute)) { + // 1. Abort these steps and ignore the cookie entirely. return; + } + // Otherwise: + else { + // 1. Set the cookie's host-only-flag to false. + cookie.host_only = false; - // Set the cookie's host-only-flag to false. Set the cookie's domain to the domain-attribute. - cookie.host_only = false; - } else { - // Set the cookie's host-only-flag to true. Set the cookie's domain to the canonicalized request-host. + // 2. Set the cookie's domain to the domain-attribute. + cookie.domain = move(domain_attribute); + } + } + // Otherwise: + else { + // 1. Set the cookie's host-only-flag to true. cookie.host_only = true; + + // 2. Set the cookie's domain to the canonicalized request-host. cookie.domain = move(canonicalized_domain); } - // 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Path": + // 11. If the cookie-attribute-list contains an attribute with an attribute-name of "Path", set the cookie's path to + // attribute-value of the last attribute in the cookie-attribute-list with both an attribute-name of "Path" and + // an attribute-value whose length is no more than 1024 octets. Otherwise, set the cookie's path to the + // default-path of the request-uri. if (parsed_cookie.path.has_value()) { - // Set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path". - cookie.path = parsed_cookie.path.value(); + if (parsed_cookie.path->byte_count() <= 1024) + cookie.path = parsed_cookie.path.value(); } else { cookie.path = default_path(url); } - // 8. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's secure-only-flag to true. + // 12. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's + // secure-only-flag to true. Otherwise, set the cookie's secure-only-flag to false. cookie.secure = parsed_cookie.secure_attribute_present; - // 9. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's http-only-flag to false. + // 13. If the request-uri does not denote a "secure" connection (as defined by the user agent), and the cookie's + // secure-only-flag is true, then abort these steps and ignore the cookie entirely. + if (cookie.secure && url.scheme() != "https"sv) + return; + + // 14. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's + // http-only-flag to true. Otherwise, set the cookie's http-only-flag to false. cookie.http_only = parsed_cookie.http_only_attribute_present; - // 10. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is set, abort these steps and ignore the cookie entirely. - if (source != Web::Cookie::Source::Http && cookie.http_only) + // 15. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is true, abort these steps + // and ignore the cookie entirely. + if (source == Web::Cookie::Source::NonHttp && cookie.http_only) return; + // 16. If the cookie's secure-only-flag is false, and the request-uri does not denote a "secure" connection, then + // abort these steps and ignore the cookie entirely if the cookie store contains one or more cookies that meet + // all of the following criteria: + if (!cookie.secure && url.scheme() != "https"sv) { + auto ignore_cookie = false; + + m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie const& old_cookie) { + // 1. Their name matches the name of the newly-created cookie. + if (old_cookie.name != cookie.name) + return IterationDecision::Continue; + + // 2. Their secure-only-flag is true. + if (!old_cookie.secure) + return IterationDecision::Continue; + + // 3. Their domain domain-matches the domain of the newly-created cookie, or vice-versa. + if (!domain_matches(old_cookie.domain, cookie.domain) && !domain_matches(cookie.domain, old_cookie.domain)) + return IterationDecision::Continue; + + // 4. The path of the newly-created cookie path-matches the path of the existing cookie. + if (!path_matches(cookie.path, old_cookie.path)) + return IterationDecision::Continue; + + ignore_cookie = true; + return IterationDecision::Break; + }); + + if (ignore_cookie) + return; + } + + // 17. If the cookie-attribute-list contains an attribute with an attribute-name of "SameSite", and an + // attribute-value of "Strict", "Lax", or "None", set the cookie's same-site-flag to the attribute-value of the + // last attribute in the cookie-attribute-list with an attribute-name of "SameSite". Otherwise, set the cookie's + // same-site-flag to "Default". + cookie.same_site = parsed_cookie.same_site_attribute; + + // 18. If the cookie's same-site-flag is not "None": + if (cookie.same_site != Web::Cookie::SameSite::None) { + // FIXME: 1. If the cookie was received from a "non-HTTP" API, and the API was called from a navigable's active document + // whose "site for cookies" is not same-site with the top-level origin, then abort these steps and ignore the + // newly created cookie entirely. + + // FIXME: 2. If the cookie was received from a "same-site" request (as defined in Section 5.2), skip the remaining + // substeps and continue processing the cookie. + + // FIXME: 3. If the cookie was received from a request which is navigating a top-level traversable [HTML] (e.g. if the + // request's "reserved client" is either null or an environment whose "target browsing context"'s navigable + // is a top-level traversable), skip the remaining substeps and continue processing the cookie. + + // FIXME: 4. Abort these steps and ignore the newly created cookie entirely. + } + + // 19. If the cookie's "same-site-flag" is "None", abort these steps and ignore the cookie entirely unless the + // cookie's secure-only-flag is true. + if (cookie.same_site == Web::Cookie::SameSite::None && !cookie.secure) + return; + + auto has_case_insensitive_prefix = [&](StringView value, StringView prefix) { + if (value.length() < prefix.length()) + return false; + + value = value.substring_view(0, prefix.length()); + return value.equals_ignoring_ascii_case(prefix); + }; + + // 20. If the cookie-name begins with a case-insensitive match for the string "__Secure-", abort these steps and + // ignore the cookie entirely unless the cookie's secure-only-flag is true. + if (has_case_insensitive_prefix(cookie.name, "__Secure-"sv) && !cookie.secure) + return; + + // 21. If the cookie-name begins with a case-insensitive match for the string "__Host-", abort these steps and + // ignore the cookie entirely unless the cookie meets all the following criteria: + if (has_case_insensitive_prefix(cookie.name, "__Host-"sv)) { + // 1. The cookie's secure-only-flag is true. + if (!cookie.secure) + return; + + // 2. The cookie's host-only-flag is true. + if (!cookie.host_only) + return; + + // 3. The cookie-attribute-list contains an attribute with an attribute-name of "Path", and the cookie's path is /. + if (parsed_cookie.path.has_value() && parsed_cookie.path != "/"sv) + return; + } + + // 22. If the cookie-name is empty and either of the following conditions are true, abort these steps and ignore + // the cookie entirely: + if (cookie.name.is_empty()) { + // * the cookie-value begins with a case-insensitive match for the string "__Secure-" + if (has_case_insensitive_prefix(cookie.value, "__Secure-"sv)) + return; + + // * the cookie-value begins with a case-insensitive match for the string "__Host-" + if (has_case_insensitive_prefix(cookie.value, "__Host-"sv)) + return; + } + CookieStorageKey key { cookie.name, cookie.domain, cookie.path }; - // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: - if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value()) { - // If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these - // steps and ignore the newly created cookie entirely. - if (source != Web::Cookie::Source::Http && old_cookie->http_only) + // 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the + // newly-created cookie: + if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) { + // 1. Let old-cookie be the existing cookie with the same name, domain, host-only-flag, and path as the + // newly-created cookie. (Notice that this algorithm maintains the invariant that there is at most one such + // cookie.) + + // 2. If the newly-created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is true, + // abort these steps and ignore the newly created cookie entirely. + if (source == Web::Cookie::Source::NonHttp && old_cookie->http_only) return; - // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. + // 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie. cookie.creation_time = old_cookie->creation_time; - // Remove the old-cookie from the cookie store. + // 4. Remove the old-cookie from the cookie store. // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. } - // 12. Insert the newly created cookie into the cookie store. + // 24. Insert the newly-created cookie into the cookie store. m_transient_storage.set_cookie(move(key), move(cookie)); m_transient_storage.purge_expired_cookies(); } +// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3 Vector CookieJar::get_matching_cookies(const URL::URL& url, StringView canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode) { - // https://tools.ietf.org/html/rfc6265#section-5.4 auto now = UnixDateTime::now(); // 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements: Vector cookie_list; - m_transient_storage.for_each_cookie([&](auto& cookie) { - // Either: The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain. - // Or: The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain. + m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie& cookie) { + // * Either: + // The cookie's host-only-flag is true and the canonicalized host of the retrieval's URI is identical to + // the cookie's domain. bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain); + // Or: + // The cookie's host-only-flag is false and the canonicalized host of the retrieval's URI domain-matches + // the cookie's domain. bool is_not_host_only_and_domain_matches = !cookie.host_only && domain_matches(canonicalized_domain, cookie.domain); + if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches) return; - // The request-uri's path path-matches the cookie's path. + // * The retrieval's URI's path path-matches the cookie's path. if (!path_matches(url.serialize_path(), cookie.path)) return; - // If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol. - if (cookie.secure && (url.scheme() != "https")) + // * If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection (as + // defined by the user agent). + if (cookie.secure && url.scheme() != "https"sv) return; - // If the cookie's http-only-flag is true, then exclude the cookie if the cookie-string is being generated for a "non-HTTP" API. + // * If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP". if (cookie.http_only && (source != Web::Cookie::Source::Http)) return; + // FIXME: * If the cookie's same-site-flag is not "None" and the retrieval's same-site status is "cross-site", then + // exclude the cookie unless all of the following conditions are met: + // * The retrieval's type is "HTTP". + // * The same-site-flag is "Lax" or "Default". + // * The HTTP request associated with the retrieval uses a "safe" method. + // * The target browsing context of the HTTP request associated with the retrieval is the active browsing context + // or a top-level traversable. + // NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies. if (mode == MatchingCookiesSpecMode::WebDriver) { cookie_list.append(cookie); @@ -424,20 +623,23 @@ Vector CookieJar::get_matching_cookies(const URL::URL& url, // NOTE: We do this first so that both our internal storage and cookie-list are updated. cookie.last_access_time = now; - // 2. The user agent SHOULD sort the cookie-list in the following order: - // - Cookies with longer paths are listed before cookies with shorter paths. - // - Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times. + // 2. The user agent SHOULD sort the cookie-list in the following order: auto cookie_path_length = cookie.path.bytes().size(); auto cookie_creation_time = cookie.creation_time; cookie_list.insert_before_matching(cookie, [cookie_path_length, cookie_creation_time](auto const& entry) { + // * Cookies with longer paths are listed before cookies with shorter paths. if (cookie_path_length > entry.path.bytes().size()) { return true; } + + // * Among cookies that have equal-length path fields, cookies with earlier creation-times are listed + // before cookies with later creation-times. if (cookie_path_length == entry.path.bytes().size()) { if (cookie_creation_time < entry.creation_time) return true; } + return false; }); }); diff --git a/Userland/Libraries/LibWebView/CookieJar.h b/Userland/Libraries/LibWebView/CookieJar.h index f5d5684776c..fd1cb49a303 100644 --- a/Userland/Libraries/LibWebView/CookieJar.h +++ b/Userland/Libraries/LibWebView/CookieJar.h @@ -54,8 +54,17 @@ class CookieJar { template void for_each_cookie(Callback callback) { - for (auto& it : m_cookies) - callback(it.value); + using ReturnType = InvokeResult; + + for (auto& it : m_cookies) { + if constexpr (IsSame) { + if (callback(it.value) == IterationDecision::Break) + return; + } else { + static_assert(IsSame); + callback(it.value); + } + } } private: