Commit 4c5f863f authored by Lily Chen's avatar Lily Chen Committed by Commit Bot

Refactor CookieMonster::MaybeDeleteEquivalentCookieAndUpdateStatus

This change straightens out some of the convoluted logic in this method,
so that hopefully it is simpler and easier to understand.

Bug: None
Change-Id: I913e98c04c3f9f3a8c6727a25c6c0f4675695c77
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1877664
Commit-Queue: Lily Chen <chlily@chromium.org>
Reviewed-by: default avatarMaksim Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/master@{#710884}
parent 38f707fe
...@@ -1012,109 +1012,100 @@ void CookieMonster::FilterCookiesWithOptions( ...@@ -1012,109 +1012,100 @@ void CookieMonster::FilterCookiesWithOptions(
void CookieMonster::MaybeDeleteEquivalentCookieAndUpdateStatus( void CookieMonster::MaybeDeleteEquivalentCookieAndUpdateStatus(
const std::string& key, const std::string& key,
const CanonicalCookie& ecc, const CanonicalCookie& cookie_being_set,
bool source_secure, bool source_secure,
bool skip_httponly, bool skip_httponly,
bool already_expired, bool already_expired,
base::Time* creation_date_to_inherit, base::Time* creation_date_to_inherit,
CanonicalCookie::CookieInclusionStatus* status) { CanonicalCookie::CookieInclusionStatus* status) {
DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!status->HasExclusionReason(
CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE));
DCHECK(!status->HasExclusionReason(
CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY));
bool found_equivalent_cookie = false; bool found_equivalent_cookie = false;
bool skipped_httponly = false; CookieMap::iterator maybe_delete_it = cookies_.end();
bool skipped_secure_cookie = false;
CookieMap::iterator cookie_it_to_possibly_delete = cookies_.end();
CanonicalCookie* cc_skipped_secure = nullptr; CanonicalCookie* cc_skipped_secure = nullptr;
for (CookieMapItPair its = cookies_.equal_range(key);
its.first != its.second;) {
auto curit = its.first;
CanonicalCookie* cc = curit->second.get();
++its.first;
// Check every cookie matching this domain key for equivalence.
CookieMapItPair range_its = cookies_.equal_range(key);
for (auto cur_it = range_its.first; cur_it != range_its.second; ++cur_it) {
CanonicalCookie* cc = cur_it->second.get();
// Evaluate "Leave Secure Cookies Alone":
// If the cookie is being set from an insecure scheme, then if a cookie // If the cookie is being set from an insecure scheme, then if a cookie
// already exists with the same name and it is Secure, then the cookie // already exists with the same name and it is Secure, then the cookie
// should *not* be updated if they domain-match and ignoring the path // should *not* be updated if they domain-match and ignoring the path
// attribute. // attribute. This notion of equivalence is slightly more inclusive than the
// usual IsEquivalent() check.
// //
// See: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone // See: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone
if (cc->IsSecure() && !source_secure && if (cc->IsSecure() && !source_secure &&
ecc.IsEquivalentForSecureCookieMatching(*cc)) { cookie_being_set.IsEquivalentForSecureCookieMatching(*cc)) {
skipped_secure_cookie = true; // Hold onto this for additional Netlogging later if we end up preserving
// a would-have-been-deleted cookie because of this.
cc_skipped_secure = cc; cc_skipped_secure = cc;
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE, net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE,
[&](NetLogCaptureMode capture_mode) { [&](NetLogCaptureMode capture_mode) {
return NetLogCookieMonsterCookieRejectedSecure( return NetLogCookieMonsterCookieRejectedSecure(
cc, &ecc, capture_mode); cc_skipped_secure, &cookie_being_set,
capture_mode);
}); });
// If the cookie is equivalent to the new cookie and wouldn't have been status->AddExclusionReason(
// skipped for being HTTP-only, record that it is a skipped secure cookie CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE);
// that would have been deleted otherwise. }
if (ecc.IsEquivalent(*cc)) {
found_equivalent_cookie = true; if (cookie_being_set.IsEquivalent(*cc)) {
// Would also have skipped for being httponly, so make a note of that.
if (skip_httponly && cc->IsHttpOnly())
skipped_httponly = true;
}
} else if (ecc.IsEquivalent(*cc)) {
// We should never have more than one equivalent cookie, since they should // We should never have more than one equivalent cookie, since they should
// overwrite each other, unless secure cookies require secure scheme is // overwrite each other.
// being enforced. In that case, cookies with different paths might exist
// and be considered equivalent.
CHECK(!found_equivalent_cookie) CHECK(!found_equivalent_cookie)
<< "Duplicate equivalent cookies found, cookie store is corrupted."; << "Duplicate equivalent cookies found, cookie store is corrupted.";
DCHECK(cookie_it_to_possibly_delete == cookies_.end()); DCHECK(maybe_delete_it == cookies_.end());
found_equivalent_cookie = true;
// The |cookie_being_set| is rejected for trying to overwrite an httponly
// cookie when it should not be able to.
if (skip_httponly && cc->IsHttpOnly()) { if (skip_httponly && cc->IsHttpOnly()) {
skipped_httponly = true;
net_log_.AddEvent( net_log_.AddEvent(
NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY,
[&](NetLogCaptureMode capture_mode) { [&](NetLogCaptureMode capture_mode) {
return NetLogCookieMonsterCookieRejectedHttponly(cc, &ecc, return NetLogCookieMonsterCookieRejectedHttponly(
capture_mode); cc, &cookie_being_set, capture_mode);
}); });
status->AddExclusionReason(CanonicalCookie::CookieInclusionStatus::
EXCLUDE_OVERWRITE_HTTP_ONLY);
} else { } else {
cookie_it_to_possibly_delete = curit; maybe_delete_it = cur_it;
} }
found_equivalent_cookie = true;
} }
} }
if (cookie_it_to_possibly_delete != cookies_.end()) { if (maybe_delete_it != cookies_.end()) {
CanonicalCookie* cc_to_possibly_delete = CanonicalCookie* maybe_delete_cc = maybe_delete_it->second.get();
cookie_it_to_possibly_delete->second.get(); if (status->IsInclude()) {
// 1) If a secure cookie was encountered (and left alone), don't actually if (maybe_delete_cc->Value() == cookie_being_set.Value())
// modify any of the pre-existing cookies. Only delete if no secure cookies *creation_date_to_inherit = maybe_delete_cc->CreationDate();
// were skipped. 2) Only delete if the status of the current cookie-addition InternalDeleteCookie(maybe_delete_it, true,
// is "include", so that we don't throw out a valid cookie for a bad cookie.
if (!skipped_secure_cookie && status->IsInclude()) {
if (cc_to_possibly_delete->Value() == ecc.Value()) {
*creation_date_to_inherit = cc_to_possibly_delete->CreationDate();
}
InternalDeleteCookie(cookie_it_to_possibly_delete, true,
already_expired ? DELETE_COOKIE_EXPIRED_OVERWRITE already_expired ? DELETE_COOKIE_EXPIRED_OVERWRITE
: DELETE_COOKIE_OVERWRITE); : DELETE_COOKIE_OVERWRITE);
} else if (skipped_secure_cookie) { } else if (status->HasExclusionReason(
// If any secure cookie was skipped, preserve the pre-existing cookie. CanonicalCookie::CookieInclusionStatus::
EXCLUDE_OVERWRITE_SECURE)) {
// Log that we preserved a cookie that would have been deleted due to
// Leave Secure Cookies Alone. This arbitrarily only logs the last
// |cc_skipped_secure| that we were left with after the for loop, even if
// there were multiple matching Secure cookies that were left alone.
DCHECK(cc_skipped_secure); DCHECK(cc_skipped_secure);
net_log_.AddEvent( net_log_.AddEvent(
NetLogEventType::COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE, NetLogEventType::COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE,
[&](NetLogCaptureMode capture_mode) { [&](NetLogCaptureMode capture_mode) {
return NetLogCookieMonsterCookiePreservedSkippedSecure( return NetLogCookieMonsterCookiePreservedSkippedSecure(
cc_skipped_secure, cc_to_possibly_delete, &ecc, capture_mode); cc_skipped_secure, maybe_delete_cc, &cookie_being_set,
capture_mode);
}); });
} }
} }
if (skipped_httponly) {
status->AddExclusionReason(
CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY);
}
if (skipped_secure_cookie) {
status->AddExclusionReason(
CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE);
}
} }
CookieMonster::CookieMap::iterator CookieMonster::InternalInsertCookie( CookieMonster::CookieMap::iterator CookieMonster::InternalInsertCookie(
......
...@@ -397,18 +397,24 @@ class NET_EXPORT CookieMonster : public CookieStore { ...@@ -397,18 +397,24 @@ class NET_EXPORT CookieMonster : public CookieStore {
CookieStatusList* included_cookies, CookieStatusList* included_cookies,
CookieStatusList* excluded_cookies); CookieStatusList* excluded_cookies);
// Delete any cookies that are equivalent to |ecc| (same path, domain, etc). // Possibly delete an existing cookie equivalent to |cookie_being_set| (same
// path, domain, and name).
//
// |source_secure| indicates if the source may override existing secure // |source_secure| indicates if the source may override existing secure
// cookies. // cookies. If the source is not secure, and there is an existing "equivalent"
// cookie that is Secure, that cookie will be preserved, under "Leave Secure
// Cookies Alone" (see
// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone-01).
// ("equivalent" here is in quotes because the equivalency check for the
// purposes of preserving existing Secure cookies is slightly more inclusive.)
// //
// If |skip_httponly| is true, httponly cookies will not be deleted. The // If |skip_httponly| is true, httponly cookies will not be deleted even if
// return value will be true if |skip_httponly| skipped an httponly cookie or // they are equivalent.
// the cookie to delete was Secure and the scheme of |ecc| is insecure. |key| // |key| is the key to find the cookie in cookies_; see the comment before the
// is the key to find the cookie in cookies_; see the comment before the
// CookieMap typedef for details. // CookieMap typedef for details.
// //
// If a cookie is deleted, and its value matches |ecc|'s value, then // If a cookie is deleted, and its value matches |cookie_being_set|'s value,
// |creation_date_to_inherit| will be set to that cookie's creation date. // then |creation_date_to_inherit| will be set to that cookie's creation date.
// //
// The cookie will not be deleted if |*status| is not "include" when calling // The cookie will not be deleted if |*status| is not "include" when calling
// the function. The function will update |*status| with exclusion reasons if // the function. The function will update |*status| with exclusion reasons if
...@@ -417,7 +423,7 @@ class NET_EXPORT CookieMonster : public CookieStore { ...@@ -417,7 +423,7 @@ class NET_EXPORT CookieMonster : public CookieStore {
// NOTE: There should never be more than a single matching equivalent cookie. // NOTE: There should never be more than a single matching equivalent cookie.
void MaybeDeleteEquivalentCookieAndUpdateStatus( void MaybeDeleteEquivalentCookieAndUpdateStatus(
const std::string& key, const std::string& key,
const CanonicalCookie& ecc, const CanonicalCookie& cookie_being_set,
bool source_secure, bool source_secure,
bool skip_httponly, bool skip_httponly,
bool already_expired, bool already_expired,
......
...@@ -2746,6 +2746,105 @@ TEST_F(CookieMonsterTest, CookieSourceHistogram) { ...@@ -2746,6 +2746,105 @@ TEST_F(CookieMonsterTest, CookieSourceHistogram) {
CookieMonster::COOKIE_SOURCE_NONSECURE_COOKIE_NONCRYPTOGRAPHIC_SCHEME, 1); CookieMonster::COOKIE_SOURCE_NONSECURE_COOKIE_NONCRYPTOGRAPHIC_SCHEME, 1);
} }
TEST_F(CookieMonsterTest, MaybeDeleteEquivalentCookieAndUpdateStatus) {
scoped_refptr<MockPersistentCookieStore> store(new MockPersistentCookieStore);
std::unique_ptr<CookieMonster> cm(new CookieMonster(store.get(), &net_log_));
// Set a secure, httponly cookie from a secure origin
auto preexisting_cookie = CanonicalCookie::Create(
https_www_foo_.url(), "A=B;Secure;HttpOnly", base::Time::Now(),
base::nullopt /* server_time */);
CanonicalCookie::CookieInclusionStatus status =
SetCanonicalCookieReturnStatus(cm.get(), std::move(preexisting_cookie),
"https", true /* can_modify_httponly */);
ASSERT_TRUE(status.IsInclude());
// Set a new cookie with a different name. Should work because cookies with
// different names are not considered equivalent nor "equivalent for secure
// cookie matching".
// Same origin:
EXPECT_TRUE(SetCookie(cm.get(), https_www_foo_.url(), "B=A;"));
// Different scheme, same domain:
EXPECT_TRUE(SetCookie(cm.get(), http_www_foo_.url(), "C=A;"));
// Set a non-Secure cookie from an insecure origin that is
// equivalent to the pre-existing Secure cookie.
auto bad_cookie =
CanonicalCookie::Create(http_www_foo_.url(), "A=D", base::Time::Now(),
base::nullopt /* server_time */);
// Allow modifying HttpOnly, so that we don't skip preexisting cookies for
// being HttpOnly.
status = SetCanonicalCookieReturnStatus(
cm.get(), std::move(bad_cookie), "http", true /* can_modify_httponly */);
EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
{CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE}));
// The preexisting cookie should still be there.
EXPECT_THAT(GetCookiesWithOptions(cm.get(), https_www_foo_.url(),
CookieOptions::MakeAllInclusive()),
::testing::HasSubstr("A=B"));
auto entries = net_log_.GetEntries();
size_t skipped_secure_netlog_index = ExpectLogContainsSomewhere(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE,
NetLogEventPhase::NONE);
EXPECT_FALSE(LogContainsEntryWithTypeAfter(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY));
ExpectLogContainsSomewhereAfter(
entries, skipped_secure_netlog_index,
NetLogEventType::COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE,
NetLogEventPhase::NONE);
net_log_.Clear();
// Set a non-secure cookie from an insecure origin that matches the name of an
// already existing cookie but is not equivalent. This should fail since it's
// trying to shadow a secure cookie.
bad_cookie = CanonicalCookie::Create(
http_www_foo_.url(), "A=E; path=/some/path", base::Time::Now(),
base::nullopt /* server_time */);
// Allow modifying HttpOnly, so that we don't skip preexisting cookies for
// being HttpOnly.
status = SetCanonicalCookieReturnStatus(
cm.get(), std::move(bad_cookie), "http", true /* can_modify_httponly */);
EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
{CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE}));
// The preexisting cookie should still be there.
EXPECT_THAT(GetCookiesWithOptions(cm.get(), https_www_foo_.url(),
CookieOptions::MakeAllInclusive()),
::testing::HasSubstr("A=B"));
entries = net_log_.GetEntries();
skipped_secure_netlog_index = ExpectLogContainsSomewhere(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE,
NetLogEventPhase::NONE);
EXPECT_FALSE(LogContainsEntryWithTypeAfter(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY));
// There wasn't actually a strictly equivalent cookie that we would have
// deleted.
EXPECT_FALSE(LogContainsEntryWithTypeAfter(
entries, skipped_secure_netlog_index,
NetLogEventType::COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE));
net_log_.Clear();
// Test skipping equivalent cookie for HttpOnly only.
bad_cookie = CanonicalCookie::Create(https_www_foo_.url(), "A=E; Secure",
base::Time::Now(),
base::nullopt /* server_time */);
status =
SetCanonicalCookieReturnStatus(cm.get(), std::move(bad_cookie), "https",
false /* can_modify_httponly */);
EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
{CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY}));
entries = net_log_.GetEntries();
ExpectLogContainsSomewhere(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY,
NetLogEventPhase::NONE);
EXPECT_FALSE(LogContainsEntryWithTypeAfter(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE));
}
// Test skipping a cookie in MaybeDeleteEquivalentCookieAndUpdateStatus for // Test skipping a cookie in MaybeDeleteEquivalentCookieAndUpdateStatus for
// multiple reasons (Secure and HttpOnly). // multiple reasons (Secure and HttpOnly).
TEST_F(CookieMonsterTest, SkipDontOverwriteForMultipleReasons) { TEST_F(CookieMonsterTest, SkipDontOverwriteForMultipleReasons) {
...@@ -2771,6 +2870,14 @@ TEST_F(CookieMonsterTest, SkipDontOverwriteForMultipleReasons) { ...@@ -2771,6 +2870,14 @@ TEST_F(CookieMonsterTest, SkipDontOverwriteForMultipleReasons) {
EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting( EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
{CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE, {CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE,
CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY})); CanonicalCookie::CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY}));
auto entries = net_log_.GetEntries();
ExpectLogContainsSomewhere(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE,
NetLogEventPhase::NONE);
ExpectLogContainsSomewhere(
entries, 0, NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY,
NetLogEventPhase::NONE);
} }
// Test that when we check for equivalent cookies, we don't remove any if the // Test that when we check for equivalent cookies, we don't remove any if the
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment