blob: 1eda2bdc39378b4f20c32ade77d433659c0f95e7 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/dips/dips_bounce_detector.h"
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/dips/dips_utils.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom-shared.h"
#include "third_party/metrics_proto/ukm/source.pb.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/test/base/android/android_browser_test.h"
#else
#include "chrome/test/base/in_process_browser_test.h"
#endif
using base::Bucket;
using blink::mojom::EngagementLevel;
using content::NavigationHandle;
using content::WebContents;
using testing::ElementsAre;
using testing::Eq;
using testing::Gt;
using testing::Pair;
using ukm::builders::DIPS_Redirect;
namespace {
class UserActivationObserver : public content::WebContentsObserver {
public:
explicit UserActivationObserver(content::WebContents* web_contents,
content::RenderFrameHost* render_frame_host)
: WebContentsObserver(web_contents),
render_frame_host_(render_frame_host) {}
// Wait until the frame receives user activation.
void Wait() { run_loop_.Run(); }
// WebContentsObserver override
void FrameReceivedUserActivation(
content::RenderFrameHost* render_frame_host) override {
if (render_frame_host_ == render_frame_host) {
run_loop_.Quit();
}
}
private:
raw_ptr<content::RenderFrameHost> const render_frame_host_;
base::RunLoop run_loop_;
};
class CookieAccessObserver : public content::WebContentsObserver {
public:
explicit CookieAccessObserver(content::WebContents* web_contents,
content::RenderFrameHost* render_frame_host)
: WebContentsObserver(web_contents),
render_frame_host_(render_frame_host) {}
// Wait until the frame accesses cookies.
void Wait() { run_loop_.Run(); }
// WebContentsObserver override
void OnCookiesAccessed(content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) override {
if (render_frame_host_ == render_frame_host) {
run_loop_.Quit();
}
}
private:
const raw_ptr<content::RenderFrameHost> render_frame_host_;
base::RunLoop run_loop_;
};
} // namespace
// Returns a simplified URL representation for ease of comparison in tests.
// Just host+path.
std::string FormatURL(const GURL& url) {
return base::StrCat({url.host_piece(), url.path_piece()});
}
// Keeps a log of DidStartNavigation, OnCookiesAccessed, and DidFinishNavigation
// executions.
class WCOCallbackLogger
: public content::WebContentsObserver,
public content::WebContentsUserData<WCOCallbackLogger> {
public:
WCOCallbackLogger(const WCOCallbackLogger&) = delete;
WCOCallbackLogger& operator=(const WCOCallbackLogger&) = delete;
const std::vector<std::string>& log() const { return log_; }
private:
explicit WCOCallbackLogger(content::WebContents* web_contents);
// So WebContentsUserData::CreateForWebContents() can call the constructor.
friend class content::WebContentsUserData<WCOCallbackLogger>;
void DidStartNavigation(NavigationHandle* navigation_handle) override;
void OnCookiesAccessed(content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) override;
void OnCookiesAccessed(NavigationHandle* navigation_handle,
const content::CookieAccessDetails& details) override;
void DidFinishNavigation(NavigationHandle* navigation_handle) override;
std::vector<std::string> log_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WCOCallbackLogger::WCOCallbackLogger(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<WCOCallbackLogger>(*web_contents) {}
void WCOCallbackLogger::DidStartNavigation(
NavigationHandle* navigation_handle) {
log_.push_back(
base::StringPrintf("DidStartNavigation(%s)",
FormatURL(navigation_handle->GetURL()).c_str()));
}
void WCOCallbackLogger::OnCookiesAccessed(
content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) {
// Callbacks for favicons are ignored only in testing logs because their
// ordering is variable and would cause flakiness
if (!render_frame_host->IsInPrimaryMainFrame() ||
FormatURL(details.url).find("favicon.ico") != std::string::npos) {
return;
}
log_.push_back(base::StringPrintf(
"OnCookiesAccessed(RenderFrameHost, %s: %s)",
details.type == content::CookieAccessDetails::Type::kChange ? "Change"
: "Read",
FormatURL(details.url).c_str()));
}
void WCOCallbackLogger::OnCookiesAccessed(
NavigationHandle* navigation_handle,
const content::CookieAccessDetails& details) {
log_.push_back(base::StringPrintf(
"OnCookiesAccessed(NavigationHandle, %s: %s)",
details.type == content::CookieAccessDetails::Type::kChange ? "Change"
: "Read",
FormatURL(details.url).c_str()));
}
void WCOCallbackLogger::DidFinishNavigation(
NavigationHandle* navigation_handle) {
// Android testing produces callbacks for a finished navigation to "blank" at
// the beginning of a test. These should be ignored here.
if (FormatURL(navigation_handle->GetURL()) == "blank" ||
navigation_handle->GetPreviousPrimaryMainFrameURL().is_empty()) {
return;
}
log_.push_back(
base::StringPrintf("DidFinishNavigation(%s)",
FormatURL(navigation_handle->GetURL()).c_str()));
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WCOCallbackLogger);
class DIPSBounceDetectorBrowserTest : public PlatformBrowserTest {
protected:
void SetUpInProcessBrowserTestFixture() override {
DIPSBounceDetector::SetTickClockForTesting(&test_clock_);
}
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("a.test", "127.0.0.1");
host_resolver()->AddRule("b.test", "127.0.0.1");
host_resolver()->AddRule("sub.b.test", "127.0.0.1");
host_resolver()->AddRule("c.test", "127.0.0.1");
host_resolver()->AddRule("d.test", "127.0.0.1");
host_resolver()->AddRule("e.test", "127.0.0.1");
host_resolver()->AddRule("f.test", "127.0.0.1");
host_resolver()->AddRule("g.test", "127.0.0.1");
bounce_detector_ =
DIPSBounceDetector::FromWebContents(GetActiveWebContents());
}
void TearDownInProcessBrowserTestFixture() override {
DIPSBounceDetector::SetTickClockForTesting(nullptr);
}
content::WebContents* GetActiveWebContents() {
return chrome_test_utils::GetActiveWebContents(this);
}
DIPSBounceDetector* bounce_detector() { return bounce_detector_; }
void SetDIPSTime(base::TimeTicks ticks) { test_clock_.SetNowTicks(ticks); }
void AdvanceDIPSTime(base::TimeDelta delta) { test_clock_.Advance(delta); }
void CreateImageAndWaitForCookieAccess(const GURL& image_url) {
WebContents* web_contents = GetActiveWebContents();
CookieAccessObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
ASSERT_TRUE(content::ExecJs(web_contents,
content::JsReplace(
R"(
let img = document.createElement('img');
img.src = $1;
document.body.appendChild(img);)",
image_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// The image must cause a cookie access, or else this will hang.
observer.Wait();
}
// Perform a browser-based navigation to terminate the current redirect chain.
// (NOTE: tests using WCOCallbackLogger must call this *after* checking the
// log, since this navigation will be logged.)
void EndRedirectChain() {
ASSERT_TRUE(content::NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/title1.html")));
}
private:
base::SimpleTestTickClock test_clock_;
raw_ptr<DIPSBounceDetector> bounce_detector_ = nullptr;
};
// The timing of WCO::OnCookiesAccessed() execution is unpredictable for
// redirects. Sometimes it's called before WCO::DidRedirectNavigation(), and
// sometimes after. Therefore DIPSBounceDetector needs to know when it's safe to
// judge an HTTP redirect as stateful (accessing cookies) or not. This test
// tries to verify that OnCookiesAccessed() is always called before
// DidFinishNavigation(), so that DIPSBounceDetector can safely perform that
// judgement in DidFinishNavigation().
//
// This test also verifies that OnCookiesAccessed() is called for URLs in the
// same order that they're visited (and that for redirects that both read and
// write cookies, OnCookiesAccessed() is called with kRead before it's called
// with kChange, although DIPSBounceDetector doesn't depend on that anymore.)
//
// If either assumption is incorrect, this test will be flaky. On 2022-04-27 I
// (rtarpine) ran this test 1000 times in 40 parallel jobs with no failures, so
// it seems robust.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AllCookieCallbacksBeforeNavigationFinished) {
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site/b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/"
"d.test/set-cookie?name=value");
GURL final_url =
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on all 4 test domains
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("c.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value")));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Visit the redirect.
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect_url, final_url));
// Verify that the 7 OnCookiesAccessed() executions are called in order, and
// all between DidStartNavigation() and DidFinishNavigation().
//
// Note: according to web_contents_observer.h, sometimes cookie reads/writes
// from navigations may cause the RenderFrameHost* overload of
// OnCookiesAccessed to be called instead. We haven't seen that yet, and this
// test will intentionally fail if it happens so that we'll notice.
EXPECT_THAT(
logger->log(),
testing::ElementsAre(
("DidStartNavigation(a.test/cross-site/b.test/cross-site-with-cookie/"
"c.test/cross-site-with-cookie/d.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, Read: "
"a.test/cross-site/b.test/cross-site-with-cookie/c.test/"
"cross-site-with-cookie/d.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, Read: "
"b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/d.test/"
"set-cookie)"),
("OnCookiesAccessed(NavigationHandle, Change: "
"b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/d.test/"
"set-cookie)"),
("OnCookiesAccessed(NavigationHandle, Read: "
"c.test/cross-site-with-cookie/d.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, Change: "
"c.test/cross-site-with-cookie/d.test/set-cookie)"),
"OnCookiesAccessed(NavigationHandle, Read: d.test/set-cookie)",
"OnCookiesAccessed(NavigationHandle, Change: d.test/set-cookie)",
"DidFinishNavigation(d.test/set-cookie)"));
}
void AppendRedirectURL(std::vector<std::string>* urls,
const DIPSRedirectInfo& redirect,
const DIPSRedirectChainInfo& chain) {
if (redirect.access_type != CookieAccessType::kNone) {
urls->push_back(
base::StrCat({FormatURL(redirect.url), " (",
CookieAccessTypeToString(redirect.access_type), ")"}));
}
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulServerRedirect_URL) {
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/d.test/"
"title1.html");
GURL final_url = embedded_test_server()->GetURL("d.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on a.test, b.test and d.test (but not c.test).
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value")));
std::vector<std::string> stateful_redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirectURL, &stateful_redirects));
// Visit the redirect.
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect_url, final_url));
EndRedirectChain();
// a.test and b.test are stateful redirects. c.test had no cookies, and d.test
// was not a redirect.
EXPECT_THAT(
stateful_redirects,
testing::ElementsAre(
("a.test/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/"
"d.test/title1.html (ReadWrite)"),
("b.test/cross-site/c.test/cross-site/d.test/title1.html (Read)")));
}
// An EmbeddedTestServer request handler for
// /cross-site-with-samesite-none-cookie URLs. Like /cross-site-with-cookie, but
// the cookie has additional Secure and SameSite=None attributes.
std::unique_ptr<net::test_server::HttpResponse>
HandleCrossSiteSameSiteNoneCookieRedirect(
net::EmbeddedTestServer* server,
const net::test_server::HttpRequest& request) {
const std::string prefix = "/cross-site-with-samesite-none-cookie";
if (!net::test_server::ShouldHandle(request, prefix))
return nullptr;
std::string dest_all = base::UnescapeBinaryURLComponent(
request.relative_url.substr(prefix.size() + 1));
std::string dest;
size_t delimiter = dest_all.find("/");
if (delimiter != std::string::npos) {
dest = base::StringPrintf(
"//%s:%hu/%s", dest_all.substr(0, delimiter).c_str(), server->port(),
dest_all.substr(delimiter + 1).c_str());
}
auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
http_response->AddCustomHeader("Location", dest);
http_response->AddCustomHeader("Set-Cookie",
"server-redirect=true; Secure; SameSite=None");
http_response->set_content_type("text/html");
http_response->set_content(base::StringPrintf(
"<html><head></head><body>Redirecting to %s</body></html>",
dest.c_str()));
return http_response;
}
// Ignore iframes because their state will be partitioned under the top-level
// site anyway.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
IgnoreServerRedirectsInIframes) {
// We host the iframe content on an HTTPS server, because for it to write a
// cookie, the cookie needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
https_server.RegisterDefaultHandler(base::BindRepeating(
&HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
ASSERT_TRUE(https_server.Start());
const GURL root_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
const GURL redirect_url = https_server.GetURL(
"b.test", "/cross-site-with-samesite-none-cookie/c.test/title1.html");
const std::string iframe_id = "test";
content::WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> stateful_redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirectURL, &stateful_redirects));
ASSERT_TRUE(content::NavigateToURL(web_contents, root_url));
ASSERT_TRUE(
content::NavigateIframeToURL(web_contents, iframe_id, redirect_url));
EndRedirectChain();
// b.test had a stateful redirect, but because it was in an iframe, we ignored
// it.
EXPECT_THAT(stateful_redirects, testing::IsEmpty());
}
void AppendRedirect(std::vector<std::string>* redirects,
const DIPSRedirectInfo& redirect,
const DIPSRedirectChainInfo& chain) {
if (redirect.access_type != CookieAccessType::kNone) {
redirects->push_back(base::StrCat({FormatURL(chain.initial_url), " -> ",
FormatURL(redirect.url), " -> ",
FormatURL(chain.final_url)}));
}
}
// Tests that a stateful client-side redirect that occurs in less than
// 10 seconds is recognized.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Client) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* frame;
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Visit initial page
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
frame = web_contents->GetPrimaryMainFrame();
// Wait for navigation to finish to initial page
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
// Wait until we can click.
content::WaitForHitTestData(frame);
// Advance TimeTicks 10 seconds
AdvanceDIPSTime(base::TimeDelta(base::Seconds(10)));
// Navigate to interstitial page via "mouse click"
UserActivationObserver observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce_url),
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS));
observer.Wait();
// Wait for navigation to finish to interstitial page
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page
CookieAccessObserver cookie_observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
// Initiate client-side redirect via JS without click
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", final_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to final page
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
EXPECT_THAT(logger->log(), testing::ElementsAre(
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
("DidStartNavigation(b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Change: b.test/title1.html)"),
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)")));
EndRedirectChain();
EXPECT_THAT(
redirects,
testing::ElementsAre(
("a.test/title1.html -> b.test/title1.html -> c.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Server) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/d.test/"
"title1.html");
GURL final_url = embedded_test_server()->GetURL("d.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on a.test, b.test and d.test (but not c.test).
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value")));
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Visit the redirect.
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect_url, final_url));
EndRedirectChain();
// a.test and b.test are stateful redirects. c.test had no cookies, and d.test
// was not a redirect.
EXPECT_THAT(redirects,
testing::ElementsAre(
("a.test/title1.html -> "
"a.test/cross-site-with-cookie/b.test/cross-site/c.test/"
"cross-site/d.test/title1.html -> d.test/title1.html"),
("a.test/title1.html -> "
"b.test/cross-site/c.test/cross-site/d.test/title1.html -> "
"d.test/title1.html")));
}
// Tests behavior for recognizing stateful client-side redirect that happens
// between stateful server-side redirects.
// TODO(https://crbug.com/1345215): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DetectStatefulRedirect_ServerClient \
DISABLED_DetectStatefulRedirect_ServerClient
#else
#define MAYBE_DetectStatefulRedirect_ServerClient \
DetectStatefulRedirect_ServerClient
#endif
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
MAYBE_DetectStatefulRedirect_ServerClient) {
GURL initial1_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL redirect1_url = embedded_test_server()->GetURL(
"a.test", "/cross-site-with-cookie/b.test/title1.html");
GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL initial2_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL redirect2_url = embedded_test_server()->GetURL(
"c.test", "/cross-site-with-cookie/d.test/title1.html");
GURL final_url = embedded_test_server()->GetURL("d.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* frame;
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Visit initial page 1.
ASSERT_TRUE(content::NavigateToURL(web_contents, initial1_url));
// Visit the redirect (a.test -> b.test with cookies).
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect1_url, bounce_url));
// Wait for navigation to finish to bounce page (b.test).
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Advance TimeTicks by 1 second.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page.
CookieAccessObserver cookie_observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
// Initiate client-side redirect via JS without click (to initial page 2).
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", initial2_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to redirect page 2 (c.test).
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
// Visit the redirect (c.test -> d.test with cookies).
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect2_url, final_url));
EXPECT_THAT(logger->log(), testing::ElementsAre(
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
("DidStartNavigation(a.test/"
"cross-site-with-cookie/b.test/title1.html)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: a.test/cross-site-with-cookie/"
"b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Change: b.test/title1.html)"),
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)"),
("DidStartNavigation(c.test/"
"cross-site-with-cookie/d.test/title1.html)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: c.test/cross-site-with-cookie/"
"d.test/title1.html)"),
("DidFinishNavigation(d.test/title1.html)")));
EndRedirectChain();
EXPECT_THAT(
redirects,
testing::ElementsAre(
("a.test/title1.html -> "
"a.test/cross-site-with-cookie/b.test/title1.html -> "
"c.test/title1.html"),
("a.test/title1.html -> b.test/title1.html -> c.test/title1.html"),
("c.test/title1.html -> "
"c.test/cross-site-with-cookie/d.test/title1.html -> "
"d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulClientRedirect_Chain) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce1_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL bounce2_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL bounce3_url = embedded_test_server()->GetURL("d.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("a.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* frame;
// a.test -(click)-> b.test -(UA-redir)-> c.test -(redir)-> d.test -(redir)->
// a.test
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
// Visit initial page.
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Wait for navigation to finish to initial page.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Wait until we can click.
content::WaitForHitTestData(frame);
// Advance TimeTicks 10 seconds.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(10)));
// Navigate to bounce page 1 via "mouse click".
UserActivationObserver observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce1_url),
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS));
observer.Wait();
// Wait for navigation to finish to bounce page 1.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Wait until we can click.
content::WaitForHitTestData(frame);
// simulate mouse click
UserActivationObserver observer2(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer2.Wait();
// Advance TimeTicks by 1 second.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page 1.
CookieAccessObserver cookie_observer1(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer1.Wait();
// Initiate client-side redirect via JS without click.
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce2_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to bounce page 2.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Advance TimeTicks by 1 second.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page 2.
CookieAccessObserver cookie_observer2(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer2.Wait();
// Initiate client-side redirect to via JS without click.
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce3_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to bounce page 3.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Advance TimeTicks by 1 second.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page 3.
CookieAccessObserver cookie_observer3(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer3.Wait();
// Initiate client-side redirect to final page via JS without click.
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", final_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to final page (a.test).
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
EndRedirectChain();
// c.test and d.test are stateful bounces, but b.test is not counted as a
// bounce because it received user activation shortly before redirecting away.
EXPECT_THAT(
redirects,
testing::ElementsAre(
("b.test/title1.html -> c.test/title1.html -> a.test/title1.html"),
("b.test/title1.html -> d.test/title1.html -> a.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulServerRedirect_NoContent) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
// The redirect chain ends in a 204 No Content response, which doesn't commit.
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/d.test/"
"nocontent");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on a.test, b.test and d.test (but not c.test).
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value")));
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Visit the redirect (note that the user ends up back at initial_url).
ASSERT_TRUE(content::NavigateToURL(web_contents, redirect_url,
/*expected_commit_url=*/initial_url));
EndRedirectChain();
// a.test and b.test are stateful redirects. c.test had no cookies, and d.test
// was not a redirect.
EXPECT_THAT(redirects,
testing::ElementsAre(
("a.test/title1.html -> "
"a.test/cross-site-with-cookie/b.test/cross-site/c.test/"
"cross-site/d.test/nocontent -> d.test/nocontent"),
("a.test/title1.html -> "
"b.test/cross-site/c.test/cross-site/d.test/nocontent -> "
"d.test/nocontent")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulServerRedirect_404Error) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
// The redirect chain ends in a 404 error.
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/d.test/404");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on a.test, b.test and d.test (but not c.test).
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value")));
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Visit the redirect, ending up on an error page.
ASSERT_FALSE(content::NavigateToURL(web_contents, redirect_url));
ASSERT_TRUE(content::IsLastCommittedEntryOfPageType(
web_contents, content::PAGE_TYPE_ERROR));
EndRedirectChain();
// a.test and b.test are stateful redirects. c.test had no cookies, and d.test
// was not a redirect.
EXPECT_THAT(redirects,
testing::ElementsAre(
("a.test/title1.html -> "
"a.test/cross-site-with-cookie/b.test/cross-site/c.test/"
"cross-site/d.test/404 -> d.test/404"),
("a.test/title1.html -> "
"b.test/cross-site/c.test/cross-site/d.test/404 -> "
"d.test/404")));
}
const std::vector<std::string>& GetAllRedirectMetrics() {
static const std::vector<std::string> kAllRedirectMetrics = {
"ClientBounceDelay",
"CookieAccessType",
"HasStickyActivation",
"InitialAndFinalSitesSame",
"RedirectAndFinalSiteSame",
"RedirectAndInitialSiteSame",
"RedirectChainIndex",
"RedirectChainLength",
"RedirectType",
"SiteEngagementLevel",
};
return kAllRedirectMetrics;
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Histograms_BounceCategory_Client) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce1_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL bounce2_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("d.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* frame;
// Set cookies on b.test. Note that browser-initiated navigations like
// these are treated as a sign of user engagement.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
// Set cookies on c.test without of user engagement signal.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents->GetPrimaryMainFrame(),
embedded_test_server()->GetURL("c.test", "/set-cookie?name=value")));
// a.test -(click) -> b.test -(redir)-> c.test -(redir) -> d.test
// Visit initial page.
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Wait for navigation to finish to initial page.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Wait until we can click.
content::WaitForHitTestData(frame);
// Advance TimeTicks 10 seconds.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(10)));
// Navigate to bounce page 1 via "mouse click" and monitor the histograms.
base::HistogramTester histograms;
ukm::TestAutoSetUkmRecorder ukm_recorder;
UserActivationObserver observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce1_url),
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS));
observer.Wait();
// Wait for navigation to finish to bounce page 1.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Wait until we can click.
content::WaitForHitTestData(frame);
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Initiate client-side redirect via JS without click.
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", bounce2_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to bounce page 2.
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
frame = web_contents->GetPrimaryMainFrame();
// Advance TimeTicks by 1 second.
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Write Cookie via JS on bounce page 2 (c.test).
CookieAccessObserver cookie_observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
// Initiate client-side redirect to final page via JS without click.
ASSERT_TRUE(content::ExecJs(
frame, content::JsReplace("window.location.href = $1;", final_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// Wait for navigation to finish to final page (d.test).
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
EndRedirectChain();
// Verify the correct histogram was used for all samples.
base::HistogramTester::CountsMap expected_counts;
expected_counts["Privacy.DIPS.BounceCategoryClient.Standard"] = 2;
EXPECT_THAT(
histograms.GetTotalCountsForPrefix("Privacy.DIPS.BounceCategoryClient."),
testing::ContainerEq(expected_counts));
// Verify the proper values were recorded. b.test is a has user engagement
// and read cookies, while c.test has no user engagement and wrote cookies.
EXPECT_THAT(
histograms.GetAllSamples("Privacy.DIPS.BounceCategoryClient.Standard"),
testing::ElementsAre(
// c.test
Bucket((int)RedirectCategory::kReadWriteCookies_NoEngagement, 1),
// b.test
Bucket((int)RedirectCategory::kReadCookies_HasEngagement, 1)));
// Verify a redirect time metric was recorded for each bounce.
histograms.ExpectBucketCount(
"Privacy.DIPS.TimeFromNavigationCommitToClientBounce",
static_cast<base::HistogramBase::Sample>(
base::Seconds(1).InMilliseconds()),
/*expected_count=*/2);
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> ukm_entries =
ukm_recorder.GetEntries("DIPS.Redirect", GetAllRedirectMetrics());
ASSERT_EQ(2u, ukm_entries.size());
EXPECT_THAT(
FormatURL(
ukm_recorder.GetSourceForSourceId(ukm_entries[0].source_id)->url()),
Eq("b.test/title1.html"));
EXPECT_THAT(ukm::GetSourceIdType(ukm_entries[0].source_id),
Eq(ukm::SourceIdType::NAVIGATION_ID));
EXPECT_THAT(
ukm_entries[0].metrics,
ElementsAre(Pair("ClientBounceDelay", 1),
Pair("CookieAccessType", (int)CookieAccessType::kRead),
Pair("HasStickyActivation", false),
Pair("InitialAndFinalSitesSame", false),
Pair("RedirectAndFinalSiteSame", false),
Pair("RedirectAndInitialSiteSame", false),
Pair("RedirectChainIndex", 0), Pair("RedirectChainLength", 2),
Pair("RedirectType", (int)DIPSRedirectType::kClient),
Pair("SiteEngagementLevel", Gt((int)EngagementLevel::NONE))));
EXPECT_THAT(
FormatURL(
ukm_recorder.GetSourceForSourceId(ukm_entries[1].source_id)->url()),
Eq("c.test/title1.html"));
EXPECT_THAT(ukm::GetSourceIdType(ukm_entries[1].source_id),
Eq(ukm::SourceIdType::NAVIGATION_ID));
EXPECT_THAT(
ukm_entries[1].metrics,
ElementsAre(Pair("ClientBounceDelay", 1),
Pair("CookieAccessType", (int)CookieAccessType::kReadWrite),
Pair("HasStickyActivation", false),
Pair("InitialAndFinalSitesSame", false),
Pair("RedirectAndFinalSiteSame", false),
Pair("RedirectAndInitialSiteSame", false),
Pair("RedirectChainIndex", 1), Pair("RedirectChainLength", 2),
Pair("RedirectType", (int)DIPSRedirectType::kClient),
Pair("SiteEngagementLevel", (int)EngagementLevel::NONE)));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Histograms_BounceCategory_Server) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/d.test/"
"title1.html");
GURL final_url = embedded_test_server()->GetURL("d.test", "/title1.html");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on a.test and b.test. Note that browser-initiated navigations
// like these are treated as a sign of user engagement.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/set-cookie?name=value")));
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("b.test", "/set-cookie?name=value")));
// Navigate to starting page.
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, initial_url));
// Visit the redirect and monitor the histograms.
base::HistogramTester histograms;
ukm::TestAutoSetUkmRecorder ukm_recorder;
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, redirect_url,
final_url));
EndRedirectChain();
// Verify the correct histogram was used for all samples.
base::HistogramTester::CountsMap expected_counts;
expected_counts["Privacy.DIPS.BounceCategoryServer.Standard"] = 2;
EXPECT_THAT(
histograms.GetTotalCountsForPrefix("Privacy.DIPS.BounceCategoryServer."),
testing::ContainerEq(expected_counts));
// Verify the proper values were recorded. Note that the a.test redirect was
// not reported because the previously committed page was also on a.test.
EXPECT_THAT(
histograms.GetAllSamples("Privacy.DIPS.BounceCategoryServer.Standard"),
testing::ElementsAre(
// c.test
Bucket((int)RedirectCategory::kNoCookies_NoEngagement, 1),
// b.test
Bucket((int)RedirectCategory::kReadCookies_HasEngagement, 1)));
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> ukm_entries =
ukm_recorder.GetEntries("DIPS.Redirect", GetAllRedirectMetrics());
ASSERT_EQ(3u, ukm_entries.size());
EXPECT_THAT(
FormatURL(
ukm_recorder.GetSourceForSourceId(ukm_entries[0].source_id)->url()),
Eq("a.test/cross-site-with-cookie/b.test/cross-site/c.test/cross-site/"
"d.test/title1.html"));
EXPECT_THAT(ukm::GetSourceIdType(ukm_entries[0].source_id),
Eq(ukm::SourceIdType::REDIRECT_ID));
EXPECT_THAT(
ukm_entries[0].metrics,
ElementsAre(Pair("ClientBounceDelay", 0),
Pair("CookieAccessType", (int)CookieAccessType::kReadWrite),
Pair("HasStickyActivation", false),
Pair("InitialAndFinalSitesSame", false),
Pair("RedirectAndFinalSiteSame", false),
Pair("RedirectAndInitialSiteSame", true),
Pair("RedirectChainIndex", 0), Pair("RedirectChainLength", 3),
Pair("RedirectType", (int)DIPSRedirectType::kServer),
Pair("SiteEngagementLevel", Gt((int)EngagementLevel::NONE))));
EXPECT_THAT(
FormatURL(
ukm_recorder.GetSourceForSourceId(ukm_entries[1].source_id)->url()),
Eq("b.test/cross-site/c.test/cross-site/d.test/title1.html"));
EXPECT_THAT(ukm::GetSourceIdType(ukm_entries[1].source_id),
Eq(ukm::SourceIdType::REDIRECT_ID));
EXPECT_THAT(
ukm_entries[1].metrics,
ElementsAre(Pair("ClientBounceDelay", 0),
Pair("CookieAccessType", (int)CookieAccessType::kRead),
Pair("HasStickyActivation", false),
Pair("InitialAndFinalSitesSame", false),
Pair("RedirectAndFinalSiteSame", false),
Pair("RedirectAndInitialSiteSame", false),
Pair("RedirectChainIndex", 1), Pair("RedirectChainLength", 3),
Pair("RedirectType", (int)DIPSRedirectType::kServer),
Pair("SiteEngagementLevel", Gt((int)EngagementLevel::NONE))));
EXPECT_THAT(
FormatURL(
ukm_recorder.GetSourceForSourceId(ukm_entries[2].source_id)->url()),
Eq("c.test/cross-site/d.test/title1.html"));
EXPECT_THAT(ukm::GetSourceIdType(ukm_entries[2].source_id),
Eq(ukm::SourceIdType::REDIRECT_ID));
EXPECT_THAT(
ukm_entries[2].metrics,
ElementsAre(Pair("ClientBounceDelay", 0),
Pair("CookieAccessType", (int)CookieAccessType::kNone),
Pair("HasStickyActivation", false),
Pair("InitialAndFinalSitesSame", false),
Pair("RedirectAndFinalSiteSame", false),
Pair("RedirectAndInitialSiteSame", false),
Pair("RedirectChainIndex", 2), Pair("RedirectChainLength", 3),
Pair("RedirectType", (int)DIPSRedirectType::kServer),
Pair("SiteEngagementLevel", (int)EngagementLevel::NONE)));
}
// This test verifies that a third-party cookie access doesn't cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(
DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Client_IgnoreThirdPartySubresource) {
// We host the image on an HTTPS server, because for it to read a third-party
// cookie, it needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
https_server.RegisterDefaultHandler(base::BindRepeating(
&HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
ASSERT_TRUE(https_server.Start());
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL image_url = https_server.GetURL("d.test", "/favicon/icon.png");
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Set SameSite=None cookie on d.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, https_server.GetURL(
"d.test", "/set-cookie?foo=bar;Secure;SameSite=None")));
// Visit initial page
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, bounce_url));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Cause a third-party cookie read.
CreateImageAndWaitForCookieAccess(image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
final_url));
EXPECT_THAT(logger->log(),
testing::ElementsAre(
// Set cookie on d.test
("DidStartNavigation(d.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: d.test/set-cookie)"),
("DidFinishNavigation(d.test/set-cookie)"),
// Visit a.test
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
// Bounce on b.test (reading third-party d.test cookie)
("DidStartNavigation(b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Read: d.test/favicon/icon.png)"),
// Land on c.test
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)")));
EndRedirectChain();
// b.test is not considered a stateful bounce.
EXPECT_THAT(redirects, testing::IsEmpty());
}
// This test verifies that a same-site cookie access DOES cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Client_FirstPartySubresource) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL image_url =
embedded_test_server()->GetURL("sub.b.test", "/favicon/icon.png");
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendRedirect, &redirects));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Set cookie on sub.b.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("sub.b.test", "/set-cookie?foo=bar")));
// Visit initial page
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, bounce_url));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::TimeDelta(base::Seconds(1)));
// Cause a same-site cookie read.
CreateImageAndWaitForCookieAccess(image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
final_url));
EXPECT_THAT(logger->log(),
testing::ElementsAre(
// Set cookie on sub.b.test
("DidStartNavigation(sub.b.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: sub.b.test/set-cookie)"),
("DidFinishNavigation(sub.b.test/set-cookie)"),
// Visit a.test
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
// Bounce on b.test (reading same-site sub.b.test cookie)
("DidStartNavigation(b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Read: sub.b.test/favicon/icon.png)"),
// Land on c.test
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)")));
EndRedirectChain();
// b.test IS considered a stateful bounce, even though the cookie was read by
// an image hosted on sub.b.test.
EXPECT_THAT(
redirects,
testing::ElementsAre(
("a.test/title1.html -> b.test/title1.html -> c.test/title1.html")));
}
void AppendAnyRedirect(std::vector<std::string>* redirects,
const DIPSRedirectInfo& redirect,
const DIPSRedirectChainInfo& chain) {
redirects->push_back(base::StringPrintf(
"[%d/%d] %s -> %s (%s) -> %s", redirect.index + 1, chain.length,
FormatURL(chain.initial_url).c_str(), FormatURL(redirect.url).c_str(),
CookieAccessTypeToString(redirect.access_type).data(),
FormatURL(chain.final_url).c_str()));
}
// This test verifies that consecutive redirect chains are combined into one.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_ServerClientClientServer) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendAnyRedirect, &redirects));
// Visit initial page on a.test
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::Seconds(1));
// Navigate without a click (i.e. by C-redirecting) to d.test
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::Seconds(1));
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// S-redirects to f.test
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL("e.test",
"/cross-site/f.test/title1.html"),
embedded_test_server()->GetURL("f.test", "/title1.html")));
EndRedirectChain();
EXPECT_THAT(redirects,
testing::ElementsAre(
("[1/4] a.test/title1.html -> "
"b.test/cross-site/c.test/title1.html (None) -> "
"f.test/title1.html"),
("[2/4] a.test/title1.html -> c.test/title1.html (None) -> "
"f.test/title1.html"),
("[3/4] a.test/title1.html -> d.test/title1.html (None) -> "
"f.test/title1.html"),
("[4/4] a.test/title1.html -> "
"e.test/cross-site/f.test/title1.html (None) -> "
"f.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_UncommittedChain) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendAnyRedirect, &redirects));
// Visit initial page on a.test
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::Seconds(1));
// Navigate without a click (i.e. by C-redirecting) to d.test which redirects
// to e.test but doesn't commit.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL("d.test", "/cross-site/e.test/nocontent"),
/* empty URL since the navigation doesn't commit: */ GURL::EmptyGURL()));
ASSERT_EQ("c.test/title1.html",
FormatURL(web_contents->GetLastCommittedURL()));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::Seconds(1));
// Navigate with a click (not a redirect) to d.test which redirects
// to e.test but doesn't commit.
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("d.test", "/cross-site/e.test/nocontent"),
/* empty URL since the navigation doesn't commit: */ GURL::EmptyGURL()));
ASSERT_EQ("c.test/title1.html",
FormatURL(web_contents->GetLastCommittedURL()));
// Advance TimeTicks by 1 second
AdvanceDIPSTime(base::Seconds(1));
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// S-redirects to f.test
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL("e.test",
"/cross-site/f.test/title1.html"),
embedded_test_server()->GetURL("f.test", "/title1.html")));
EndRedirectChain();
EXPECT_THAT(redirects,
testing::ElementsAre(
// First uncommitted chain (with client redirect):
("[2/3] a.test/title1.html -> "
"c.test/title1.html (None) -> "
"e.test/nocontent"),
("[3/3] a.test/title1.html -> "
"d.test/cross-site/e.test/nocontent (None) -> "
"e.test/nocontent"),
// Second uncommitted chain (without client redirect):
("[1/1] c.test/title1.html -> "
"d.test/cross-site/e.test/nocontent (None) -> "
"e.test/nocontent"),
// Finally the committed chain:
("[1/3] a.test/title1.html -> "
"b.test/cross-site/c.test/title1.html (None) -> "
"f.test/title1.html"),
("[2/3] a.test/title1.html -> c.test/title1.html (None) -> "
"f.test/title1.html"),
("[3/3] a.test/title1.html -> "
"e.test/cross-site/f.test/title1.html (None) -> "
"f.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_ClosingTabEndsChain) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
bounce_detector()->SetRedirectHandlerForTesting(
base::BindRepeating(&AppendAnyRedirect, &redirects));
// Visit initial page on a.test
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
content::WebContentsDestroyedWatcher destruction_watcher(web_contents);
web_contents->Close();
destruction_watcher.Wait();
EXPECT_THAT(redirects, testing::ElementsAre(
("[1/1] a.test/title1.html -> "
"b.test/cross-site/c.test/title1.html (None) -> "
"c.test/title1.html")));
}