blob: 9d9ea982530d32e9eafb13f19ebb193f67520965 [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 "content/browser/btm/btm_bounce_detector.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/content_settings/core/common/features.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/ukm/content/source_url_recorder.h"
#include "content/browser/btm/btm_browsertest_utils.h"
#include "content/browser/btm/btm_service_impl.h"
#include "content/browser/btm/btm_storage.h"
#include "content/browser/btm/btm_test_utils.h"
#include "content/browser/btm/btm_utils.h"
#include "content/browser/tpcd_heuristics/opener_heuristic_tab_helper.h"
#include "content/browser/tpcd_heuristics/redirect_heuristic_tab_helper.h"
#include "content/common/features.h"
#include "content/public/browser/attribution_data_model.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/btm_redirect_info.h"
#include "content/public/browser/btm_service.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/interest_group_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/btm_service_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/site_for_cookies.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.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 "net/test/test_data_directory.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "services/network/public/cpp/features.h"
#include "services/network/test/trust_token_request_handler.h"
#include "services/network/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-shared.h"
#include "url/gurl.h"
#include "url/origin.h"
#if !BUILDFLAG(IS_ANDROID)
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#endif // !BUILDFLAG(IS_ANDROID)
using base::Bucket;
using testing::Contains;
using testing::ElementsAre;
using testing::Eq;
using testing::Gt;
using testing::IsEmpty;
using testing::Pair;
namespace content {
namespace {
using AttributionData = std::set<AttributionDataModel::DataKey>;
using blink::mojom::StorageTypeAccessed;
// 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()});
}
void AppendRedirect(std::vector<std::string>* redirects,
const BtmRedirectInfo& redirect,
const BtmRedirectChainInfo& chain,
size_t redirect_index) {
redirects->push_back(base::StringPrintf(
"[%zu/%zu] %s -> %s (%s) -> %s", redirect_index + 1, chain.length,
FormatURL(chain.initial_url).c_str(),
FormatURL(redirect.redirector_url).c_str(),
std::string(BtmDataAccessTypeToString(redirect.access_type)).c_str(),
FormatURL(chain.final_url).c_str()));
}
void AppendRedirects(std::vector<std::string>* vec,
std::vector<BtmRedirectInfoPtr> redirects,
BtmRedirectChainInfoPtr chain) {
size_t redirect_index = chain->length - redirects.size();
for (const auto& redirect : redirects) {
AppendRedirect(vec, *redirect, *chain, redirect_index);
redirect_index++;
}
}
void AppendSitesInReport(std::vector<std::string>* reports,
const std::set<std::string>& sites) {
reports->push_back(base::JoinString(
std::vector<std::string_view>(sites.begin(), sites.end()), ", "));
}
std::vector<url::Origin> GetOrigins(const AttributionData& data) {
std::vector<url::Origin> origins;
std::ranges::transform(data, std::back_inserter(origins),
&AttributionDataModel::DataKey::reporting_origin);
return origins;
}
bool ContainsWrite(BtmDataAccessType access) {
using enum BtmDataAccessType;
return access == kWrite || access == kReadWrite;
}
// Waits for BTM to know that a cookie was written by a redirect at
// `redirect_url`, which must be the last redirect that was performed in the
// currently-in-progress redirect chain.
testing::AssertionResult WaitForRedirectCookieWrite(WebContents* web_contents,
const GURL& redirect_url) {
RedirectChainDetector* detector =
RedirectChainDetector::FromWebContents(web_contents);
if (detector->CommittedRedirectContext().GetRedirectChainLength() == 0) {
return testing::AssertionFailure() << "No redirects detected";
}
// Make sure the last redirect was at the expected URL.
const BtmRedirectInfo& redirect =
detector->CommittedRedirectContext()
[detector->CommittedRedirectContext().size() - 1];
if (redirect.redirector_url != redirect_url) {
return testing::AssertionFailure()
<< "Expected redirect at " << redirect_url << "; found "
<< redirect.redirector_url;
}
if (!ContainsWrite(redirect.access_type)) {
// We haven't been notified about the cookie write from the redirect yet.
// Wait for it to ensure the bounce is considered stateful.
URLCookieAccessObserver(web_contents, redirect_url,
CookieOperation::kChange)
.Wait();
}
// Return success if the cookie write was detected.
if (ContainsWrite(redirect.access_type)) {
return testing::AssertionSuccess();
} else {
return testing::AssertionFailure() << "Still no cookie write detected";
}
}
} // namespace
// Keeps a log of DidStartNavigation, OnCookiesAccessed, and DidFinishNavigation
// executions.
class WCOCallbackLogger : public WebContentsObserver,
public WebContentsUserData<WCOCallbackLogger>,
public SharedWorkerService::Observer,
public DedicatedWorkerService::Observer {
public:
WCOCallbackLogger(const WCOCallbackLogger&) = delete;
WCOCallbackLogger& operator=(const WCOCallbackLogger&) = delete;
const std::vector<std::string>& log() const { return log_; }
private:
explicit WCOCallbackLogger(WebContents* web_contents);
// So WebContentsUserData::CreateForWebContents() can call the constructor.
friend class WebContentsUserData<WCOCallbackLogger>;
// Start WebContentsObserver overrides:
void DidStartNavigation(NavigationHandle* navigation_handle) override;
void OnCookiesAccessed(RenderFrameHost* render_frame_host,
const CookieAccessDetails& details) override;
void OnCookiesAccessed(NavigationHandle* navigation_handle,
const CookieAccessDetails& details) override;
void NotifyStorageAccessed(RenderFrameHost* render_frame_host,
StorageTypeAccessed storage_type,
bool blocked) override;
void OnServiceWorkerAccessed(RenderFrameHost* render_frame_host,
const GURL& scope,
AllowServiceWorkerResult allowed) override;
void OnServiceWorkerAccessed(NavigationHandle* navigation_handle,
const GURL& scope,
AllowServiceWorkerResult allowed) override;
void DidFinishNavigation(NavigationHandle* navigation_handle) override;
void WebAuthnAssertionRequestSucceeded(
RenderFrameHost* render_frame_host) override;
// End WebContentsObserver overrides.
// Start SharedWorkerService.Observer overrides:
void OnClientAdded(const blink::SharedWorkerToken& token,
GlobalRenderFrameHostId render_frame_host_id) override;
void OnWorkerCreated(const blink::SharedWorkerToken& token,
int worker_process_id,
const url::Origin& security_origin,
const base::UnguessableToken& dev_tools_token) override {
}
void OnBeforeWorkerDestroyed(const blink::SharedWorkerToken& token) override {
}
void OnClientRemoved(const blink::SharedWorkerToken& token,
GlobalRenderFrameHostId render_frame_host_id) override {}
using SharedWorkerService::Observer::OnFinalResponseURLDetermined;
// End SharedWorkerService.Observer overrides.
// Start DedicatedWorkerService.Observer overrides:
void OnWorkerCreated(const blink::DedicatedWorkerToken& worker_token,
int worker_process_id,
const url::Origin& security_origin,
DedicatedWorkerCreator creator) override;
void OnBeforeWorkerDestroyed(const blink::DedicatedWorkerToken& worker_token,
DedicatedWorkerCreator creator) override {}
void OnFinalResponseURLDetermined(
const blink::DedicatedWorkerToken& worker_token,
const GURL& url) override {}
// End DedicatedWorkerService.Observer overrides.
std::vector<std::string> log_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WCOCallbackLogger::WCOCallbackLogger(WebContents* web_contents)
: WebContentsObserver(web_contents),
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(RenderFrameHost* render_frame_host,
const CookieAccessDetails& details) {
// Callbacks for favicons are ignored only in testing logs because their
// ordering is variable and would cause flakiness
if (details.url.path() == "/favicon.ico") {
return;
}
log_.push_back(base::StringPrintf(
"OnCookiesAccessed(RenderFrameHost, %s: %s)",
details.type == CookieOperation::kChange ? "Change" : "Read",
FormatURL(details.url).c_str()));
}
void WCOCallbackLogger::OnCookiesAccessed(NavigationHandle* navigation_handle,
const CookieAccessDetails& details) {
log_.push_back(base::StringPrintf(
"OnCookiesAccessed(NavigationHandle, %s: %s)",
details.type == CookieOperation::kChange ? "Change" : "Read",
FormatURL(details.url).c_str()));
}
void WCOCallbackLogger::OnServiceWorkerAccessed(
RenderFrameHost* render_frame_host,
const GURL& scope,
AllowServiceWorkerResult allowed) {
log_.push_back(
base::StringPrintf("OnServiceWorkerAccessed(RenderFrameHost: %s)",
FormatURL(scope).c_str()));
}
void WCOCallbackLogger::OnServiceWorkerAccessed(
NavigationHandle* navigation_handle,
const GURL& scope,
AllowServiceWorkerResult allowed) {
log_.push_back(
base::StringPrintf("OnServiceWorkerAccessed(NavigationHandle: %s)",
FormatURL(scope).c_str()));
}
void WCOCallbackLogger::OnClientAdded(
const blink::SharedWorkerToken& token,
GlobalRenderFrameHostId render_frame_host_id) {
RenderFrameHost* render_frame_host =
RenderFrameHost::FromID(render_frame_host_id);
GURL scope;
if (render_frame_host) {
scope = GetFirstPartyURL(*render_frame_host);
}
log_.push_back(base::StringPrintf("OnSharedWorkerClientAdded(%s)",
FormatURL(scope).c_str()));
}
void WCOCallbackLogger::OnWorkerCreated(
const blink::DedicatedWorkerToken& worker_token,
int worker_process_id,
const url::Origin& security_origin,
DedicatedWorkerCreator creator) {
const GlobalRenderFrameHostId& render_frame_host_id =
std::get<GlobalRenderFrameHostId>(creator);
RenderFrameHost* render_frame_host =
RenderFrameHost::FromID(render_frame_host_id);
GURL scope;
if (render_frame_host) {
scope = GetFirstPartyURL(*render_frame_host);
}
log_.push_back(base::StringPrintf("OnDedicatedWorkerCreated(%s)",
FormatURL(scope).c_str()));
}
void WCOCallbackLogger::DidFinishNavigation(
NavigationHandle* navigation_handle) {
if (!IsInPrimaryPage(*navigation_handle)) {
return;
}
log_.push_back(
base::StringPrintf("DidFinishNavigation(%s)",
FormatURL(navigation_handle->GetURL()).c_str()));
}
void WCOCallbackLogger::WebAuthnAssertionRequestSucceeded(
RenderFrameHost* render_frame_host) {
log_.push_back(base::StringPrintf(
"WebAuthnAssertionRequestSucceeded(%s)",
FormatURL(render_frame_host->GetLastCommittedURL()).c_str()));
}
void WCOCallbackLogger::NotifyStorageAccessed(
RenderFrameHost* render_frame_host,
StorageTypeAccessed storage_type,
bool blocked) {
log_.push_back(base::StringPrintf(
"NotifyStorageAccessed(%s: %s)", base::ToString(storage_type).c_str(),
FormatURL(render_frame_host->GetLastCommittedURL()).c_str()));
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WCOCallbackLogger);
class BtmBounceDetectorBrowserTest : public ContentBrowserTest {
protected:
BtmBounceDetectorBrowserTest()
: prerender_test_helper_(base::BindRepeating(
&BtmBounceDetectorBrowserTest::GetActiveWebContents,
base::Unretained(this))) {
enabled_features_.push_back(
{network::features::kSkipTpcdMitigationsForAds,
{{"SkipTpcdMitigationsForAdsHeuristics", "true"}}});
}
void SetUp() override {
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features_,
disabled_features_);
ContentBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Prevents flakiness by handling clicks even before content is drawn.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void SetUpOnMainThread() override {
browser_client_ =
std::make_unique<ContentBrowserTestTpcBlockingBrowserClient>();
prerender_test_helper_.RegisterServerRequestMonitor(embedded_test_server());
net::test_server::RegisterDefaultHandlers(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("*", "127.0.0.1");
// Set third-party cookies to be blocked by default. If they're not blocked
// by default, BTM will not run.
browser_client().SetBlockThirdPartyCookiesByDefault(true);
WebContents* web_contents = GetActiveWebContents();
ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
SetUpBtmWebContentsObserver();
}
void SetUpBtmWebContentsObserver() {
web_contents_observer_ =
BtmWebContentsObserver::FromWebContents(GetActiveWebContents());
CHECK(web_contents_observer_);
}
WebContents* GetActiveWebContents() { return shell()->web_contents(); }
void StartAppendingRedirectsTo(std::vector<std::string>* redirects) {
GetRedirectChainHelper()->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, redirects));
}
void StartAppendingReportsTo(std::vector<std::string>* reports) {
web_contents_observer_->SetIssueReportingCallbackForTesting(
base::BindRepeating(&AppendSitesInReport, reports));
}
// 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.)
//
// By default (when `wait`=true) this waits for the BtmService to tell
// observers that the redirect chain was handled. But some tests override
// the handling flow so that chains don't reach the service (and so observers
// are never notified). Such tests should pass `wait`=false.
void EndRedirectChain(bool wait = true) {
WebContents* web_contents = GetActiveWebContents();
BtmService* btm_service =
BtmService::Get(web_contents->GetBrowserContext());
GURL expected_url = web_contents->GetLastCommittedURL();
BtmRedirectChainObserver chain_observer(btm_service, expected_url);
// Performing a browser-based navigation terminates the current redirect
// chain.
ASSERT_TRUE(NavigateToURL(
web_contents,
embedded_test_server()->GetURL("endthechain.test", "/title1.html")));
if (wait) {
chain_observer.Wait();
}
}
auto* fenced_frame_test_helper() { return &fenced_frame_test_helper_; }
auto* prerender_test_helper() { return &prerender_test_helper_; }
RenderFrameHost* GetIFrame() {
WebContents* web_contents = GetActiveWebContents();
return ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
}
RenderFrameHost* GetNestedIFrame() { return ChildFrameAt(GetIFrame(), 0); }
RedirectChainDetector* GetRedirectChainHelper() {
return RedirectChainDetector::FromWebContents(GetActiveWebContents());
}
void NavigateNestedIFrameTo(RenderFrameHost* parent_frame,
const std::string& iframe_id,
const GURL& url) {
TestNavigationObserver load_observer(GetActiveWebContents());
std::string script = base::StringPrintf(
"var iframe = document.getElementById('%s');iframe.src='%s';",
iframe_id.c_str(), url.spec().c_str());
ASSERT_TRUE(ExecJs(parent_frame, script, EXECUTE_SCRIPT_NO_USER_GESTURE));
load_observer.Wait();
}
void AccessCHIPSViaJSIn(RenderFrameHost* frame) {
FrameCookieAccessObserver observer(GetActiveWebContents(), frame,
CookieOperation::kChange);
ASSERT_TRUE(ExecJs(frame,
"document.cookie = '__Host-foo=bar;"
"SameSite=None;Secure;Path=/;Partitioned';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
}
void SimulateMouseClick() {
SimulateMouseClickAndWait(GetActiveWebContents());
}
void SimulateCookieWrite() {
WebContents* web_contents = GetActiveWebContents();
RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
URLCookieAccessObserver cookie_observer(
web_contents, frame->GetLastCommittedURL(), CookieOperation::kChange);
ASSERT_TRUE(ExecJs(frame, "document.cookie = 'foo=bar';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
}
TpcBlockingBrowserClient& browser_client() { return browser_client_->impl(); }
const base::FilePath kContentTestDataDir = GetTestDataFilePath();
std::vector<base::test::FeatureRefAndParams> enabled_features_;
std::vector<base::test::FeatureRef> disabled_features_;
raw_ptr<BtmWebContentsObserver, AcrossTasksDanglingUntriaged>
web_contents_observer_ = nullptr;
private:
test::PrerenderTestHelper prerender_test_helper_;
test::FencedFrameTestHelper fenced_frame_test_helper_;
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<ContentBrowserTestTpcBlockingBrowserClient> browser_client_;
};
IN_PROC_BROWSER_TEST_F(
BtmBounceDetectorBrowserTest,
// TODO(crbug.com/40924446): Re-enable this test
DISABLED_AttributeSameSiteIframesCookieClientAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
AccessCookieViaJSIn(GetActiveWebContents(), GetIFrame());
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
// TODO(crbug.com/40276415): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \
DISABLED_AttributeSameSiteIframesCookieServerAccessTo1P
#else
#define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \
AttributeSameSiteIframesCookieServerAccessTo1P
#endif
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
browser_client().AllowThirdPartyCookiesOnSite(
embedded_test_server()->GetURL("a.test", "/"));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
Attribute3PIframesCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = https_server.GetURL("b.test", "/title1.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
AccessCHIPSViaJSIn(GetIFrame());
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
std::string access_type =
base::FeatureList::IsEnabled(network::features::kGetCookiesOnSet)
? "ReadWrite"
: "Write";
EXPECT_THAT(redirects,
ElementsAre(base::StringPrintf(
"[1/1] blank -> a.test/page_with_blank_iframe.html "
"(%s) -> d.test/title1.html",
access_type)));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
// TODO(crbug.com/40287072): Re-enable this test
DISABLED_Attribute3PIframesCHIPSServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
https_server.GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
https_server.GetURL("b.test",
"/set-cookie?__Host-foo=bar;SameSite=None;"
"Secure;Path=/;Partitioned");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(
BtmBounceDetectorBrowserTest,
// TODO(crbug.com/40287072): Re-enable this test
DISABLED_AttributeSameSiteNestedIframesCookieClientAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
AccessCookieViaJSIn(GetActiveWebContents(), GetNestedIFrame());
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(
BtmBounceDetectorBrowserTest,
// TODO(crbug.com/40287072): Re-enable this test
DISABLED_AttributeSameSiteNestedIframesCookieServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url =
https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
Attribute3PNestedIframesCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("b.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url = https_server.GetURL("c.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
AccessCHIPSViaJSIn(GetNestedIFrame());
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
std::string access_type =
base::FeatureList::IsEnabled(network::features::kGetCookiesOnSet)
? "ReadWrite"
: "Write";
EXPECT_THAT(redirects,
ElementsAre(base::StringPrintf(
"[1/1] blank -> a.test/page_with_blank_iframe.html "
"(%s) -> d.test/title1.html",
access_type)));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
Attribute3PNestedIframesCHIPSServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("b.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url = https_server.GetURL(
"a.test",
"/set-cookie?__Host-foo=bar;SameSite=None;Secure;Path=/;Partitioned");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
Attribute3PSubResourceCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// This block represents a navigation sequence with a CHIP access (write). It
// might as well be happening in a separate tab from the navigation block
// below that does the CHIP's read via subresource request.
{
const GURL primary_main_frame_url = embedded_test_server()->GetURL(
"a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = embedded_test_server()->GetURL(
"b.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url =
https_server.GetURL("c.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
AccessCHIPSViaJSIn(GetNestedIFrame());
}
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
GURL image_url = https_server.GetURL("c.test", "/favicon/icon.png");
CreateImageAndWaitForCookieAccess(GetActiveWebContents(), image_url);
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
("[1/1] a.test/page_with_blank_iframe.html -> "
"a.test/page_with_blank_iframe.html (Read) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DiscardFencedFrameCookieClientAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL fenced_frame_url =
embedded_test_server()->GetURL("a.test", "/fenced_frames/title1.html");
RenderFrameHostWrapper fenced_frame(
fenced_frame_test_helper()->CreateFencedFrame(
GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
EXPECT_FALSE(fenced_frame.IsDestroyed());
AccessCookieViaJSIn(GetActiveWebContents(), fenced_frame.get());
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DiscardFencedFrameCookieServerAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL fenced_frame_url = embedded_test_server()->GetURL(
"a.test", "/fenced_frames/set_cookie_header.html");
URLCookieAccessObserver observer(GetActiveWebContents(), fenced_frame_url,
CookieOperation::kChange);
RenderFrameHostWrapper fenced_frame(
fenced_frame_test_helper()->CreateFencedFrame(
GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
EXPECT_FALSE(fenced_frame.IsDestroyed());
observer.Wait();
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
"[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html"));
}
// TODO(crbug.com/40917101): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
DISABLED_DiscardPrerenderedPageCookieClientAccess
#else
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
DiscardPrerenderedPageCookieClientAccess
#endif
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
MAYBE_DiscardPrerenderedPageCookieClientAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL prerendering_url =
embedded_test_server()->GetURL("a.test", "/title2.html");
const FrameTreeNodeId host_id =
prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
test::PrerenderHostObserver observer(*GetActiveWebContents(), host_id);
EXPECT_FALSE(observer.was_activated());
RenderFrameHost* prerender_frame =
prerender_test_helper()->GetPrerenderedMainFrameHost(host_id);
EXPECT_NE(prerender_frame, nullptr);
AccessCookieViaJSIn(GetActiveWebContents(), prerender_frame);
prerender_test_helper()->CancelPrerenderedPage(host_id);
observer.WaitForDestroyed();
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
"[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html"));
}
// TODO(crbug.com/40269306): flaky test.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DISABLED_DiscardPrerenderedPageCookieServerAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL prerendering_url =
embedded_test_server()->GetURL("a.test", "/set_cookie_header.html");
URLCookieAccessObserver observer(GetActiveWebContents(), prerendering_url,
CookieOperation::kChange);
const FrameTreeNodeId host_id =
prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
observer.Wait();
test::PrerenderHostObserver prerender_observer(*GetActiveWebContents(),
host_id);
EXPECT_FALSE(prerender_observer.was_activated());
prerender_test_helper()->CancelPrerenderedPage(host_id);
prerender_observer.WaitForDestroyed();
const GURL primary_main_frame_final_url =
embedded_test_server()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
// From the time the cookie was set by the prerendering page and now, the
// primary main page might have accessed (read) the cookie (when sending a
// request for a favicon after prerendering page already accessed (Write) the
// cookie). To prevent flakiness we check for any such access and test for the
// expected outcome accordingly.
// TODO(crbug.com/40269100): Investigate whether Prerendering pages
// (same-site) can be use for evasion.
const std::string expected_access_type =
observer.CookieAccessedInPrimaryPage() ? "Read" : "None";
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/title1.html (" +
expected_access_type + ") -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DetectStatefulBounce_ClientRedirect_SiteDataAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Navigate to the initial page, a.test.
ASSERT_TRUE(
NavigateToURL(GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not considered to be redirect) to b.test.
ASSERT_TRUE(NavigateToURLFromRenderer(
GetActiveWebContents(),
embedded_test_server()->GetURL("b.test", "/title1.html")));
EXPECT_TRUE(AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(),
StorageTypeAccessed::kLocalStorage));
// Navigate without a click (considered a client-redirect) to c.test.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(),
embedded_test_server()->GetURL("c.test", "/title1.html")));
EndRedirectChain(/*wait=*/false);
EXPECT_THAT(redirects,
ElementsAre("[1/1] a.test/title1.html -> b.test/title1.html "
"(Write) -> c.test/title1.html"));
}
// The timing of WCO::OnCookiesAccessed() execution is unpredictable for
// redirects. Sometimes it's called before WCO::DidRedirectNavigation(), and
// sometimes after. Therefore BtmBounceDetector 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 BtmBounceDetector 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 BtmBounceDetector 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(BtmBounceDetectorBrowserTest,
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");
WebContents* web_contents = GetActiveWebContents();
// Set cookies on all 4 test domains
ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
"a.test",
/*is_secure_cookie_set=*/false,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
"b.test",
/*is_secure_cookie_set=*/false,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
"c.test",
/*is_secure_cookie_set=*/false,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
"d.test",
/*is_secure_cookie_set=*/false,
/*is_ad_tagged=*/false));
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Visit the redirect.
URLCookieAccessObserver observer(web_contents, final_url,
CookieOperation::kChange);
ASSERT_TRUE(NavigateToURL(web_contents, redirect_url, final_url));
observer.Wait();
// 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::ContainerEq(std::vector<std::string>(
{("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)"})));
}
// 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(BtmBounceDetectorBrowserTest,
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(kContentTestDataDir);
https_server.RegisterDefaultHandler(base::BindRepeating(
&HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
ASSERT_TRUE(https_server.Start());
const GURL root_url =
embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.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_iframe";
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
ASSERT_TRUE(NavigateToURL(web_contents, root_url));
ASSERT_TRUE(NavigateIframeToURL(web_contents, iframe_id, redirect_url));
EndRedirectChain(/*wait=*/false);
// b.test had a stateful redirect, but because it was in an iframe, we ignored
// it.
EXPECT_THAT(redirects, IsEmpty());
}
// This test verifies that sites in a redirect chain with previous user
// interaction are not reported in the resulting issue when a navigation
// finishes.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
ReportRedirectorsInChain_OmitSitesWithInteraction) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> reports;
StartAppendingReportsTo(&reports);
// Record user activation on d.test.
GURL url = embedded_test_server()->GetURL("d.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, url));
SimulateMouseClick();
// Verify interaction was recorded for d.test, before proceeding.
std::optional<StateValue> state =
GetBtmState(GetBtmService(web_contents), url);
ASSERT_TRUE(state.has_value());
ASSERT_TRUE(state->user_activation_times.has_value());
// Visit initial page on a.test.
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which statefully
// S-redirects to c.test and write a cookie on c.test.
ASSERT_TRUE(NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL(
"b.test", "/cross-site-with-cookie/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());
// Navigate without a click (i.e. by C-redirecting) to d.test and write a
// cookie on d.test:
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// statelessly S-redirects to f.test, which statefully S-redirects to g.test.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL(
"e.test",
"/cross-site/f.test/cross-site-with-cookie/g.test/"
"title1.html"),
embedded_test_server()->GetURL("g.test", "/title1.html")));
EndRedirectChain();
WaitOnStorage(GetBtmService(web_contents));
// Verify that d.test is not reported (because it had previous user
// interaction), but the rest of the chain is reported.
EXPECT_THAT(reports, ElementsAre(("b.test"), ("c.test"), ("e.test, f.test")));
}
// This test verifies that a third-party cookie access doesn't cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(
BtmBounceDetectorBrowserTest,
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(kContentTestDataDir);
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;
StartAppendingRedirectsTo(&redirects);
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Set SameSite=None cookie on d.test.
ASSERT_TRUE(NavigateToURL(
web_contents, https_server.GetURL(
"d.test", "/set-cookie?foo=bar;Secure;SameSite=None")));
// Visit initial page
ASSERT_TRUE(NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
// Cause a third-party cookie read.
CreateImageAndWaitForCookieAccess(web_contents, image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
EXPECT_THAT(logger->log(),
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(/*wait=*/false);
// b.test is a bounce, but not stateful.
EXPECT_THAT(redirects, ElementsAre("[1/1] a.test/title1.html"
" -> b.test/title1.html (None)"
" -> c.test/title1.html"));
}
// This test verifies that a same-site cookie access DOES cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
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;
StartAppendingRedirectsTo(&redirects);
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Set cookie on sub.b.test.
ASSERT_TRUE(NavigateToURL(
web_contents,
embedded_test_server()->GetURL("sub.b.test", "/set-cookie?foo=bar")));
// Visit initial page
ASSERT_TRUE(NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
// Cause a same-site cookie read.
CreateImageAndWaitForCookieAccess(web_contents, image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
EXPECT_THAT(logger->log(),
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(/*wait=*/false);
// b.test IS considered a stateful bounce, even though the cookie was read by
// an image hosted on sub.b.test.
EXPECT_THAT(redirects,
ElementsAre(("[1/1] a.test/title1.html -> b.test/title1.html "
"(Read) -> c.test/title1.html")));
}
// This test verifies that consecutive redirect chains are combined into one.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DetectStatefulRedirect_ServerClientClientServer) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Visit initial page on a.test
ASSERT_TRUE(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(NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
// Navigate without a click (i.e. by C-redirecting) to d.test
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// S-redirects to f.test
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL("e.test",
"/cross-site/f.test/title1.html"),
embedded_test_server()->GetURL("f.test", "/title1.html")));
EndRedirectChain(/*wait=*/false);
EXPECT_THAT(
redirects,
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(BtmBounceDetectorBrowserTest,
DetectStatefulRedirect_ClosingTabEndsChain) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Visit initial page on a.test
ASSERT_TRUE(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(NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
EXPECT_THAT(redirects, IsEmpty());
CloseTab(web_contents);
EXPECT_THAT(redirects,
ElementsAre(("[1/1] a.test/title1.html -> "
"b.test/cross-site/c.test/title1.html (None) -> "
"c.test/title1.html")));
}
// Verifies server redirects that occur while opening a link in a new tab are
// properly detected.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
OpenServerRedirectURLInNewTab) {
WebContents* original_tab = GetActiveWebContents();
GURL original_tab_url(
embedded_test_server()->GetURL("a.test", "/title1.html"));
ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));
// Open a server-redirecting link in a new tab.
GURL new_tab_url(embedded_test_server()->GetURL(
"b.test", "/cross-site-with-cookie/c.test/title1.html"));
ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
OpenInNewTab(original_tab, new_tab_url));
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(new_tab, original_tab);
ASSERT_EQ(new_tab->GetLastCommittedURL(),
embedded_test_server()->GetURL("c.test", "/title1.html"));
ASSERT_TRUE(WaitForRedirectCookieWrite(new_tab, new_tab_url));
std::vector<std::string> redirects;
RedirectChainDetector* tab_web_contents_observer =
RedirectChainDetector::FromWebContents(new_tab);
tab_web_contents_observer->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
WebContentsDestroyedWatcher watcher(new_tab);
new_tab->Close();
watcher.Wait();
EXPECT_THAT(redirects,
ElementsAre((
"[1/1] a.test/ -> " /* Note: the URL's path is lost here. */
"b.test/cross-site-with-cookie/c.test/title1.html (Write) -> "
"c.test/title1.html")));
}
// Verifies client redirects that occur while opening a link in a new tab are
// properly detected.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
OpenClientRedirectURLInNewTab) {
WebContents* original_tab = GetActiveWebContents();
GURL original_tab_url(
embedded_test_server()->GetURL("a.test", "/title1.html"));
ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));
// Open link in a new tab.
GURL new_tab_url(embedded_test_server()->GetURL("b.test", "/title1.html"));
ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
OpenInNewTab(original_tab, new_tab_url));
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(original_tab, new_tab);
ASSERT_EQ(new_tab_url, new_tab->GetLastCommittedURL());
std::vector<std::string> redirects;
RedirectChainDetector* tab_web_contents_observer =
RedirectChainDetector::FromWebContents(new_tab);
tab_web_contents_observer->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
// Navigate without a click (i.e. by C-redirecting) to c.test.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
new_tab, embedded_test_server()->GetURL("c.test", "/title1.html")));
WebContentsDestroyedWatcher watcher(new_tab);
new_tab->Close();
watcher.Wait();
EXPECT_THAT(
redirects,
ElementsAre(("[1/1] a.test/ -> " /* Note: the URL's path is lost here. */
"b.test/title1.html (None) -> "
"c.test/title1.html")));
}
// Verifies the start URL of a redirect chain started by opening a link in a new
// tab is handled correctly, when that start page has an opaque origin.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
OpenRedirectURLInNewTab_OpaqueOriginInitiator) {
WebContents* original_tab = GetActiveWebContents();
GURL original_tab_url("data:text/html,<html></html>");
ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));
// Open a server-redirecting link in a new tab.
GURL new_tab_url(embedded_test_server()->GetURL(
"b.test", "/cross-site-with-cookie/c.test/title1.html"));
ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
OpenInNewTab(original_tab, new_tab_url));
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(new_tab, original_tab);
ASSERT_EQ(new_tab->GetLastCommittedURL(),
embedded_test_server()->GetURL("c.test", "/title1.html"));
ASSERT_TRUE(WaitForRedirectCookieWrite(new_tab, new_tab_url));
std::vector<std::string> redirects;
RedirectChainDetector::FromWebContents(new_tab)
->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
WebContentsDestroyedWatcher watcher(new_tab);
new_tab->Close();
watcher.Wait();
EXPECT_THAT(redirects,
ElementsAre((
"[1/1] blank -> "
"b.test/cross-site-with-cookie/c.test/title1.html (Write) -> "
"c.test/title1.html")));
}
class RedirectHeuristicBrowserTest : public ContentBrowserTest {
public:
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("*", "127.0.0.1");
}
void PreRunTestOnMainThread() override {
ContentBrowserTest::PreRunTestOnMainThread();
ukm::InitializeSourceUrlRecorderForWebContents(GetActiveWebContents());
browser_client_.emplace();
}
WebContents* GetActiveWebContents() { return shell()->web_contents(); }
// Perform a browser-based navigation to terminate the current redirect chain.
void EndRedirectChain() {
ASSERT_TRUE(NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("endthechain.test", "/title1.html")));
}
void SimulateMouseClick() {
SimulateMouseClickAndWait(GetActiveWebContents());
}
void SimulateWebAuthnAssertion() {
WebAuthnAssertionRequestSucceeded(
GetActiveWebContents()->GetPrimaryMainFrame());
}
TpcBlockingBrowserClient& browser_client() { return browser_client_->impl(); }
private:
std::optional<ContentBrowserTestTpcBlockingBrowserClient> browser_client_;
};
// Tests the conditions for recording RedirectHeuristic_CookieAccess2 and
// RedirectHeuristic_CookieAccessThirdParty2 UKM events.
// TODO(crbug.com/369920781): Flaky
IN_PROC_BROWSER_TEST_F(RedirectHeuristicBrowserTest,
DISABLED_RecordsRedirectHeuristicCookieAccessEvent) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
WebContents* web_contents = GetActiveWebContents();
// We host the "image" 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(GetTestDataFilePath());
ASSERT_TRUE(https_server.Start());
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL tracker_url_pre_target_redirect =
embedded_test_server()->GetURL("b.test", "/title1.html");
GURL image_url_pre_target_redirect =
https_server.GetURL("sub.b.test", "/favicon/icon.png");
GURL target_url = embedded_test_server()->GetURL("d.test", "/title1.html");
GURL target_image_url =
https_server.GetURL("sub.d.test", "/favicon/icon.png");
GURL tracker_url_post_target_redirect =
embedded_test_server()->GetURL("c.test", "/title1.html");
GURL image_url_post_target_redirect =
https_server.GetURL("sub.c.test", "/favicon/icon.png");
GURL final_url = embedded_test_server()->GetURL("f.test", "/title1.html");
browser_client().AllowThirdPartyCookiesOnSite(target_url);
// Set cookies on image URLs.
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.b.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.c.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.d.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/false));
// Visit initial page.
ASSERT_TRUE(NavigateToURL(web_contents, initial_url));
// Redirect to tracking URL.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, tracker_url_pre_target_redirect));
// Redirect to target URL.
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, target_url));
// Read a cookie from the tracking URL.
CreateImageAndWaitForCookieAccess(web_contents,
image_url_pre_target_redirect);
// Read a cookie from the second tracking URL.
CreateImageAndWaitForCookieAccess(web_contents,
image_url_post_target_redirect);
// Read a cookie from an image with the same domain as the target URL.
CreateImageAndWaitForCookieAccess(web_contents, target_image_url);
// Redirect to second tracking URL. (This has no effect since the cookie
// accesses already happened.)
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, tracker_url_post_target_redirect));
// Redirect to final URL.
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
EndRedirectChain();
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry>
ukm_first_party_entries =
ukm_recorder.GetEntries("RedirectHeuristic.CookieAccess2", {});
// Expect one UKM entry.
// Include the cookies read where a tracking site read cookies while embedded
// on a site later in the redirect chain.
// Exclude the cookies reads where:
// - The tracking site did not appear in the prior redirect chain.
// - The tracking and target sites had the same domain.
ASSERT_EQ(1u, ukm_first_party_entries.size());
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_first_party_entries[0].source_id)
->url(),
Eq(target_url));
// Expect one corresponding UKM entry for CookieAccessThirdParty.
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry>
ukm_third_party_entries = ukm_recorder.GetEntries(
"RedirectHeuristic.CookieAccessThirdParty2", {});
ASSERT_EQ(1u, ukm_third_party_entries.size());
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_third_party_entries[0].source_id)
->url(),
Eq(tracker_url_pre_target_redirect));
}
// Tests setting different metrics for the RedirectHeuristic_CookieAccess2 UKM
// event.
// TODO(crbug.com/40934961): Flaky on multiple platforms.
IN_PROC_BROWSER_TEST_F(RedirectHeuristicBrowserTest,
DISABLED_RedirectHeuristicCookieAccessEvent_AllMetrics) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
WebContents* web_contents = GetActiveWebContents();
// We host the "image" 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(GetTestDataFilePath());
ASSERT_TRUE(https_server.Start());
GURL final_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL tracker_url_with_user_activation_interaction =
embedded_test_server()->GetURL("b.test", "/title1.html");
GURL image_url_with_user_activation_interaction =
https_server.GetURL("sub.b.test", "/favicon/icon.png");
GURL tracker_url_in_iframe =
embedded_test_server()->GetURL("c.test", "/title1.html");
GURL image_url_in_iframe =
https_server.GetURL("sub.c.test", "/favicon/icon.png");
GURL tracker_url_with_authentication_interaction =
embedded_test_server()->GetURL("d.test", "/title1.html");
GURL image_url_with_authentication_interaction =
https_server.GetURL("sub.d.test", "/favicon/icon.png");
GURL target_url_3pc_allowed =
embedded_test_server()->GetURL("e.test", "/title1.html");
GURL target_url_3pc_blocked =
embedded_test_server()->GetURL("f.test", "/page_with_blank_iframe.html");
browser_client().AllowThirdPartyCookiesOnSite(target_url_3pc_allowed);
browser_client().BlockThirdPartyCookiesOnSite(target_url_3pc_blocked);
// Set cookies on image URLs.
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.b.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/true));
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.c.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/false));
ASSERT_TRUE(NavigateToSetCookie(web_contents, &https_server, "sub.d.test",
/*is_secure_cookie_set=*/true,
/*is_ad_tagged=*/false));
// Start on `tracker_url_with_user_activation_interaction` and record a
// current user activation interaction.
ASSERT_TRUE(NavigateToURL(web_contents,
tracker_url_with_user_activation_interaction));
SimulateMouseClick();
// Redirect to on `tracker_url_with_authentication_interaction` and record a
// current authentication interaction.
ASSERT_TRUE(
NavigateToURL(web_contents, tracker_url_with_authentication_interaction));
SimulateWebAuthnAssertion();
// Redirect to one of the target URLs, to set DoesFirstPartyPrecedeThirdParty.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, target_url_3pc_blocked));
// Redirect to all tracking URLs.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, tracker_url_in_iframe));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, tracker_url_with_user_activation_interaction));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, tracker_url_with_authentication_interaction));
// Redirect to target URL with cookies allowed.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, target_url_3pc_allowed));
// Read a cookie from the tracking URL with user activation interaction.
CreateImageAndWaitForCookieAccess(
web_contents,
https_server.GetURL("sub.b.test", "/favicon/icon.png?isad=1"));
// Read a cookie from the tracking URL with authentication interaction.
CreateImageAndWaitForCookieAccess(web_contents,
image_url_with_authentication_interaction);
// Redirect to target URL with cookies blocked.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, target_url_3pc_blocked));
// Open an iframe of the tracking URL on the target URL.
ASSERT_TRUE(NavigateIframeToURL(web_contents,
/*iframe_id=*/"test_iframe",
image_url_in_iframe));
// Read a cookie from the tracking URL in an iframe on the target page.
CreateImageAndWaitForCookieAccess(web_contents, image_url_in_iframe);
// Redirect to final URL.
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
EndRedirectChain();
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> ukm_entries =
ukm_recorder.GetEntries(
"RedirectHeuristic.CookieAccess2",
{"AccessId", "AccessAllowed", "IsAdTagged",
"HoursSinceLastInteraction", "MillisecondsSinceRedirect",
"OpenerHasSameSiteIframe", "SitesPassedCount",
"DoesFirstPartyPrecedeThirdParty", "IsCurrentInteraction",
"InteractionType"});
// Expect UKM entries from all three cookie accesses.
ASSERT_EQ(3u, ukm_entries.size());
// Expect reasonable delays between the redirect and cookie access.
for (const auto& entry : ukm_entries) {
EXPECT_GT(entry.metrics.at("MillisecondsSinceRedirect"), 0);
EXPECT_LT(entry.metrics.at("MillisecondsSinceRedirect"), 1000);
}
// The first cookie access was from a tracking site with a user activation
// interaction within the last hour, on a site with 3PC access allowed.
// 2 site were passed: tracker_url_with_user_activation_interaction ->
// tracker_url_with_authentication_interaction -> target_url_3pc_allowed
auto access_id_1 = ukm_entries[0].metrics.at("AccessId");
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_entries[0].source_id)->url(),
Eq(target_url_3pc_allowed));
EXPECT_EQ(ukm_entries[0].metrics.at("AccessAllowed"), true);
EXPECT_EQ(ukm_entries[0].metrics.at("IsAdTagged"),
static_cast<int32_t>(OptionalBool::kTrue));
EXPECT_EQ(ukm_entries[0].metrics.at("HoursSinceLastInteraction"), 0);
EXPECT_EQ(ukm_entries[0].metrics.at("OpenerHasSameSiteIframe"),
static_cast<int32_t>(OptionalBool::kFalse));
EXPECT_EQ(ukm_entries[0].metrics.at("SitesPassedCount"), 2);
EXPECT_EQ(ukm_entries[0].metrics.at("DoesFirstPartyPrecedeThirdParty"),
false);
EXPECT_EQ(ukm_entries[0].metrics.at("IsCurrentInteraction"), 1);
EXPECT_EQ(ukm_entries[0].metrics.at("InteractionType"),
static_cast<int32_t>(BtmInteractionType::UserActivation));
// The second cookie access was from a tracking site with an authentication
// within the last hour, on a site with 3PC access allowed.
// 1 site was passed: tracker_url_with_authentication_interaction ->
// target_url_3pc_allowed
auto access_id_2 = ukm_entries[1].metrics.at("AccessId");
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_entries[1].source_id)->url(),
Eq(target_url_3pc_allowed));
EXPECT_EQ(ukm_entries[1].metrics.at("AccessAllowed"), true);
EXPECT_EQ(ukm_entries[0].metrics.at("IsAdTagged"),
static_cast<int32_t>(OptionalBool::kFalse));
EXPECT_EQ(ukm_entries[1].metrics.at("HoursSinceLastInteraction"), 0);
EXPECT_EQ(ukm_entries[1].metrics.at("OpenerHasSameSiteIframe"),
static_cast<int32_t>(OptionalBool::kFalse));
EXPECT_EQ(ukm_entries[1].metrics.at("SitesPassedCount"), 1);
EXPECT_EQ(ukm_entries[1].metrics.at("DoesFirstPartyPrecedeThirdParty"),
false);
EXPECT_EQ(ukm_entries[1].metrics.at("IsCurrentInteraction"), 1);
EXPECT_EQ(ukm_entries[1].metrics.at("InteractionType"),
static_cast<int32_t>(BtmInteractionType::Authentication));
// The third cookie access was from a tracking site in an iframe of the
// target, on a site with 3PC access blocked.
// 4 sites were passed: tracker_url_in_iframe ->
// tracker_url_with_user_activation_interaction
// -> tracker_url_with_authentication_interaction -> target_url_3pc_allowed ->
// target_url_3pc_blocked
auto access_id_3 = ukm_entries[2].metrics.at("AccessId");
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_entries[2].source_id)->url(),
Eq(target_url_3pc_blocked));
EXPECT_EQ(ukm_entries[2].metrics.at("AccessAllowed"), false);
EXPECT_EQ(ukm_entries[2].metrics.at("IsAdTagged"),
static_cast<int32_t>(OptionalBool::kFalse));
EXPECT_EQ(ukm_entries[2].metrics.at("HoursSinceLastInteraction"), -1);
EXPECT_EQ(ukm_entries[2].metrics.at("OpenerHasSameSiteIframe"),
static_cast<int32_t>(OptionalBool::kTrue));
EXPECT_EQ(ukm_entries[2].metrics.at("SitesPassedCount"), 4);
EXPECT_EQ(ukm_entries[2].metrics.at("DoesFirstPartyPrecedeThirdParty"), true);
EXPECT_EQ(ukm_entries[2].metrics.at("IsCurrentInteraction"), 0);
EXPECT_EQ(ukm_entries[2].metrics.at("InteractionType"),
static_cast<int32_t>(BtmInteractionType::NoInteraction));
// Verify there are 3 corresponding CookieAccessThirdParty entries with
// matching access IDs.
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry>
ukm_third_party_entries = ukm_recorder.GetEntries(
"RedirectHeuristic.CookieAccessThirdParty2", {"AccessId"});
ASSERT_EQ(3u, ukm_third_party_entries.size());
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_third_party_entries[0].source_id)
->url(),
Eq(tracker_url_with_user_activation_interaction));
EXPECT_EQ(ukm_third_party_entries[0].metrics.at("AccessId"), access_id_1);
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_third_party_entries[1].source_id)
->url(),
Eq(tracker_url_with_authentication_interaction));
EXPECT_EQ(ukm_third_party_entries[1].metrics.at("AccessId"), access_id_2);
EXPECT_THAT(
ukm_recorder.GetSourceForSourceId(ukm_third_party_entries[2].source_id)
->url(),
Eq(tracker_url_in_iframe));
EXPECT_EQ(ukm_third_party_entries[2].metrics.at("AccessId"), access_id_3);
}
struct RedirectHeuristicFlags {
bool write_redirect_grants = false;
bool require_aba_flow = true;
bool require_current_interaction = true;
bool user_activation_interaction = true;
};
// chrome/browser/ui/browser.h (for changing profile prefs) is not available on
// Android.
#if !BUILDFLAG(IS_ANDROID)
class RedirectHeuristicGrantTest
: public RedirectHeuristicBrowserTest,
public testing::WithParamInterface<RedirectHeuristicFlags> {
public:
RedirectHeuristicGrantTest() {
std::string grant_time_string =
GetParam().write_redirect_grants ? "60s" : "0s";
std::string require_aba_flow_string =
base::ToString(GetParam().require_aba_flow);
std::string require_current_interaction_string =
base::ToString(GetParam().require_current_interaction);
enabled_features_.push_back(
{content_settings::features::kTpcdHeuristicsGrants,
{{"TpcdReadHeuristicsGrants", "true"},
{"TpcdWriteRedirectHeuristicGrants", grant_time_string},
{"TpcdRedirectHeuristicRequireABAFlow", require_aba_flow_string},
{"TpcdRedirectHeuristicRequireCurrentInteraction",
require_current_interaction_string}}});
}
void SetUp() override {
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features_,
disabled_features_);
RedirectHeuristicBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Prevents flakiness by handling clicks even before content is drawn.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void SetUpOnMainThread() override {
RedirectHeuristicBrowserTest::SetUpOnMainThread();
browser_client_.emplace();
browser_client().SetBlockThirdPartyCookiesByDefault(true);
WebContents* web_contents = GetActiveWebContents();
ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
}
TpcBlockingBrowserClient& browser_client() { return browser_client_->impl(); }
base::test::ScopedFeatureList scoped_feature_list_;
std::vector<base::test::FeatureRefAndParams> enabled_features_;
std::vector<base::test::FeatureRef> disabled_features_;
private:
std::optional<ContentBrowserTestTpcBlockingBrowserClient> browser_client_;
};
IN_PROC_BROWSER_TEST_P(RedirectHeuristicGrantTest,
CreatesRedirectHeuristicGrantsWithSatisfyingURL) {
WebContents* web_contents = GetActiveWebContents();
// Initialize first party URL and two trackers.
GURL first_party_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
GURL aba_current_interaction_url =
embedded_test_server()->GetURL("b.test", "/title1.html");
GURL no_interaction_url =
embedded_test_server()->GetURL("c.test", "/title1.html");
// Start on `first_party_url`.
ASSERT_TRUE(NavigateToURL(web_contents, first_party_url));
// Navigate to `aba_current_interaction_url` and record a current interaction.
ASSERT_TRUE(NavigateToURL(web_contents, aba_current_interaction_url));
SimulateMouseClick();
// Redirect through `first_party_url`, `aba_current_interaction_url`, and
// `no_interaction_url` before committing and ending on `first_party_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
first_party_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, aba_current_interaction_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
no_interaction_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
first_party_url));
EndRedirectChain();
// Wait on async tasks for the grants to be created.
WaitOnStorage(GetBtmService(web_contents));
// Expect some cookie grants on `first_party_url` based on flags and criteria.
EXPECT_EQ(browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents,
aba_current_interaction_url,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(first_party_url)),
/*overrides=*/{}),
GetParam().write_redirect_grants);
EXPECT_FALSE(browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents, no_interaction_url,
blink::StorageKey::CreateFirstParty(url::Origin::Create(first_party_url)),
/*overrides=*/{}));
}
IN_PROC_BROWSER_TEST_P(
RedirectHeuristicGrantTest,
CreatesRedirectHeuristicGrantsWithPartiallySatisfyingURL) {
WebContents* web_contents = GetActiveWebContents();
// Initialize first party URL and two trackers.
GURL first_party_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
GURL aba_past_interaction_url =
embedded_test_server()->GetURL("b.test", "/title1.html");
GURL no_aba_current_interaction_url =
embedded_test_server()->GetURL("c.test", "/title1.html");
// Record a past interaction on `aba_past_interaction_url`.
ASSERT_TRUE(NavigateToURL(web_contents, aba_past_interaction_url));
SimulateMouseClick();
// Start redirect chain on `no_aba_current_interaction_url` and record a
// current interaction.
ASSERT_TRUE(NavigateToURL(web_contents, no_aba_current_interaction_url));
SimulateMouseClick();
// Redirect through `no_aba_current_interaction_url`, `first_party_url`, and
// `aba_past_interaction_url` before committing and ending on
// `first_party_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, no_aba_current_interaction_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
first_party_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, aba_past_interaction_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
first_party_url));
EndRedirectChain();
// Wait on async tasks for the grants to be created.
WaitOnStorage(GetBtmService(web_contents));
// Expect some cookie grants on `first_party_url` based on flags and criteria.
EXPECT_EQ(browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents,
aba_past_interaction_url,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(first_party_url)),
/*overrides=*/{}),
GetParam().write_redirect_grants &&
!GetParam().require_current_interaction);
EXPECT_EQ(browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents,
no_aba_current_interaction_url,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(first_party_url)),
/*overrides=*/{}),
GetParam().write_redirect_grants && !GetParam().require_aba_flow);
}
IN_PROC_BROWSER_TEST_P(RedirectHeuristicGrantTest,
CreatesRedirectHeuristicGrantsWithWebAuthnInteractions) {
WebContents* web_contents = GetActiveWebContents();
// Initialize first party URL and two trackers.
GURL first_party_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
GURL past_interaction_url =
embedded_test_server()->GetURL("b.test", "/title1.html");
GURL current_interaction_url =
embedded_test_server()->GetURL("c.test", "/title1.html");
// Record a past web authentication interaction on `past_interaction_url`.
ASSERT_TRUE(NavigateToURL(web_contents, past_interaction_url));
SimulateWebAuthnAssertion();
// Start redirect chain on `first_party_url` with an interaction that simulate
// a user starting the authentication process
ASSERT_TRUE(NavigateToURL(web_contents, first_party_url));
SimulateMouseClick();
// Navigate through 'past_interaction_url', 'current_interaction_url' with a
// web authentication interaction, and back to 'first_party_url'
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, past_interaction_url));
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, current_interaction_url));
SimulateWebAuthnAssertion();
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents,
first_party_url));
EndRedirectChain();
// Wait on async tasks for the grants to be created.
WaitOnStorage(GetBtmService(web_contents));
// Expect some cookie grants on `first_party_url` based on flags and criteria.
EXPECT_EQ(
browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents, past_interaction_url,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(first_party_url)),
/*overrides=*/{}),
GetParam().write_redirect_grants &&
!GetParam().require_current_interaction);
EXPECT_EQ(browser_client().IsFullCookieAccessAllowed(
web_contents->GetBrowserContext(), web_contents,
current_interaction_url,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(first_party_url)),
/*overrides=*/{}),
GetParam().write_redirect_grants && !GetParam().require_aba_flow);
}
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
RedirectInfoHttpStatusPersistence) {
WebContents* const web_contents = GetActiveWebContents();
// The "final" URL will not have any server redirects.
GURL final_url = embedded_test_server()->GetURL("/echo");
// The "302" and "303" URLs will have a server redirect to the final URL,
// giving a 302 and 303 HTTP response code status, respectively.
GURL redirect_303 = embedded_test_server()->GetURL("/server-redirect-303?" +
final_url.spec());
GURL redirect_302 = embedded_test_server()->GetURL("/server-redirect-302?" +
final_url.spec());
// The "301" URL will give a 301 response code and redirect to the "302" URL.
GURL redirect_301 = embedded_test_server()->GetURL("/server-redirect-301?" +
redirect_302.spec());
// Navigate to a URL that will give a 301 redirect to another URL that will
// give a 302 redirect, before settling on a third URL.
ASSERT_TRUE(NavigateToURL(web_contents, redirect_301, final_url));
// Do client redirect to a URL that gives a 303 redirect.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, redirect_303, final_url));
RedirectChainDetector* wco =
RedirectChainDetector::FromWebContents(web_contents);
const BtmRedirectContext& context = wco->CommittedRedirectContext();
ASSERT_EQ(context.size(), 4u);
EXPECT_EQ(context[0].response_code, 301);
EXPECT_EQ(context[1].response_code, 302);
// The client redirect does not have an explicit HTTP response status.
EXPECT_EQ(context[2].response_code, 0);
EXPECT_EQ(context[3].response_code, 303);
}
const RedirectHeuristicFlags kRedirectHeuristicTestCases[] = {
{
.write_redirect_grants = false,
},
{
.write_redirect_grants = true,
.require_aba_flow = true,
.require_current_interaction = true,
},
{
.write_redirect_grants = true,
.require_aba_flow = false,
.require_current_interaction = true,
},
{
.write_redirect_grants = true,
.require_aba_flow = true,
.require_current_interaction = false,
},
{
.write_redirect_grants = true,
.require_aba_flow = false,
.require_current_interaction = false,
.user_activation_interaction = false,
},
};
INSTANTIATE_TEST_SUITE_P(All,
RedirectHeuristicGrantTest,
::testing::ValuesIn(kRedirectHeuristicTestCases));
#endif // !BUILDFLAG(IS_ANDROID)
class BtmSiteDataAccessDetectorTest
: public BtmBounceDetectorBrowserTest,
public testing::WithParamInterface<StorageTypeAccessed> {
public:
BtmSiteDataAccessDetectorTest(const BtmSiteDataAccessDetectorTest&) = delete;
BtmSiteDataAccessDetectorTest& operator=(
const BtmSiteDataAccessDetectorTest&) = delete;
BtmSiteDataAccessDetectorTest() = default;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_https_test_server().SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
embedded_https_test_server().AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(embedded_https_test_server().Start());
SetUpBtmWebContentsObserver();
}
};
IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
DetectSiteDataAccess_Storages) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
ASSERT_TRUE(NavigateToURL(
GetActiveWebContents(),
embedded_https_test_server().GetURL("a.test", "/title1.html")));
ASSERT_TRUE(
AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(), GetParam()));
ASSERT_THAT(
logger->log(),
testing::ContainerEq(std::vector<std::string>(
{"DidStartNavigation(a.test/title1.html)",
"DidFinishNavigation(a.test/title1.html)",
base::StringPrintf("NotifyStorageAccessed(%s: a.test/title1.html)",
base::ToString(GetParam()).c_str())})));
}
IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
AttributeSameSiteIframesSiteDataAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url = embedded_https_test_server().GetURL(
"a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_https_test_server().GetURL("a.test", "/title1.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
EXPECT_TRUE(AccessStorage(GetIFrame(), GetParam()));
const GURL primary_main_frame_final_url =
embedded_https_test_server().GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
AttributeSameSiteNestedIframesSiteDataAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url = embedded_https_test_server().GetURL(
"a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = embedded_https_test_server().GetURL(
"a.test", "/page_with_blank_iframe.html");
ASSERT_TRUE(
NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));
const GURL nested_iframe_url =
embedded_https_test_server().GetURL("a.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);
EXPECT_TRUE(AccessStorage(GetNestedIFrame(), GetParam()));
const GURL primary_main_frame_final_url =
embedded_https_test_server().GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects,
ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
DiscardFencedFrameCookieClientAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_https_test_server().GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL fenced_frame_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/title0.html");
std::unique_ptr<RenderFrameHostWrapper> fenced_frame =
std::make_unique<RenderFrameHostWrapper>(
fenced_frame_test_helper()->CreateFencedFrame(
GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
EXPECT_NE(fenced_frame, nullptr);
EXPECT_TRUE(AccessStorage(fenced_frame->get(), GetParam()));
const GURL primary_main_frame_final_url =
embedded_https_test_server().GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
DiscardPrerenderedPageCookieClientAccess) {
// Prerendering pages do not have access to `StorageTypeAccessed::kFileSystem`
// until activation (AKA becoming the primary page, whose test case is already
// covered).
if (GetParam() == StorageTypeAccessed::kFileSystem) {
GTEST_SKIP();
}
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_https_test_server().GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL prerendering_url =
embedded_https_test_server().GetURL("a.test", "/title2.html");
const FrameTreeNodeId host_id =
prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
test::PrerenderHostObserver observer(*GetActiveWebContents(), host_id);
EXPECT_FALSE(observer.was_activated());
RenderFrameHost* prerender_frame =
prerender_test_helper()->GetPrerenderedMainFrameHost(host_id);
EXPECT_NE(prerender_frame, nullptr);
EXPECT_TRUE(AccessStorage(prerender_frame, GetParam()));
prerender_test_helper()->CancelPrerenderedPage(host_id);
observer.WaitForDestroyed();
const GURL primary_main_frame_final_url =
embedded_https_test_server().GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(
("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}
// WeLocks accesses aren't monitored by the `PageSpecificContentSettings` as
// they are not persistent.
// TODO(crbug.com/40269763): Remove `StorageTypeAccessed::kFileSystem` once
// deprecation is complete.
INSTANTIATE_TEST_SUITE_P(All,
BtmSiteDataAccessDetectorTest,
::testing::Values(StorageTypeAccessed::kLocalStorage,
StorageTypeAccessed::kSessionStorage,
StorageTypeAccessed::kCacheStorage,
StorageTypeAccessed::kFileSystem,
StorageTypeAccessed::kIndexedDB));
// WebAuthn tests do not work on Android because there is no current way to
// install a virtual authenticator.
// NOTE: Manual testing was performed to ensure this implementation works as
// expected on Android platform.
// TODO(crbug.com/40269763): Implement automated testing once the infrastructure
// permits it (Requires mocking the Android Platform Authenticator i.e. GMS
// Core).
#if !BUILDFLAG(IS_ANDROID)
// Some refs for this test fixture:
// clang-format off
// - https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/webauthn/chrome_webauthn_browsertest.cc;drc=c4061a03f240338b42a5b84c98b1a11b62a97a9a
// - https://source.chromium.org/chromium/chromium/src/+/main:content/browser/webauth/webauth_browsertest.cc;drc=e8e4ad9096841fae7c55cea1b7d278c58f6160ff
// - https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/payments/secure_payment_confirmation_authenticator_browsertest.cc;drc=edea5c45c08d151afe67276f08a2ee13814563e1
// clang-format on
class BtmWebAuthnBrowserTest : public ContentBrowserTest {
public:
BtmWebAuthnBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
BtmWebAuthnBrowserTest(const BtmWebAuthnBrowserTest&) = delete;
BtmWebAuthnBrowserTest& operator=(const BtmWebAuthnBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
mock_cert_verifier_.SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUpInProcessBrowserTestFixture() override {
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
// Allowlist all certs for the HTTPS server.
mock_cert_verifier()->set_default_result(net::OK);
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server_.RegisterDefaultHandler(base::BindRepeating(
&HandleCrossSiteSameSiteNoneCookieRedirect, &https_server_));
ASSERT_TRUE(https_server_.Start());
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->mutable_state()->InjectResidentKey(
std::vector<uint8_t>{1, 2, 3, 4}, authn_hostname,
std::vector<uint8_t>{5, 6, 7, 8}, "Foo", "Foo Bar");
device::VirtualCtap2Device::Config config;
config.resident_key_support = true;
virtual_device_factory->SetCtap2Config(std::move(config));
auth_env_ = std::make_unique<ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
web_contents_observer_ =
BtmWebContentsObserver::FromWebContents(GetActiveWebContents());
CHECK(web_contents_observer_);
}
void TearDownOnMainThread() override {
ContentBrowserTest::TearDownOnMainThread();
web_contents_observer_ = nullptr;
}
void PostRunTestOnMainThread() override {
auth_env_.reset();
// web_contents_observer_.ClearAndDelete();
ContentBrowserTest::PostRunTestOnMainThread();
}
auto* TestServer() { return &https_server_; }
WebContents* GetActiveWebContents() { return shell()->web_contents(); }
RedirectChainDetector* GetRedirectChainHelper() {
return RedirectChainDetector::FromWebContents(GetActiveWebContents());
}
// 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(NavigateToURL(GetActiveWebContents(),
TestServer()->GetURL("a.test", "/title1.html")));
}
void StartAppendingRedirectsTo(std::vector<std::string>* redirects) {
GetRedirectChainHelper()->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, redirects));
}
void StartAppendingReportsTo(std::vector<std::string>* reports) {
web_contents_observer_->SetIssueReportingCallbackForTesting(
base::BindRepeating(&AppendSitesInReport, reports));
}
void GetWebAuthnAssertion() {
ASSERT_EQ("OK", EvalJs(GetActiveWebContents(), R"(
let cred_id = new Uint8Array([1,2,3,4]);
navigator.credentials.get({
publicKey: {
challenge: cred_id,
userVerification: 'preferred',
allowCredentials: [{
type: 'public-key',
id: cred_id,
transports: ['usb', 'nfc', 'ble'],
}],
timeout: 10000
}
}).then(c => 'OK',
e => e.toString());
)",
EXECUTE_SCRIPT_NO_USER_GESTURE));
}
ContentMockCertVerifier::CertVerifier* mock_cert_verifier() {
return mock_cert_verifier_.mock_cert_verifier();
}
protected:
const std::string authn_hostname = "b.test";
private:
ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_;
raw_ptr<BtmWebContentsObserver> web_contents_observer_ = nullptr;
std::unique_ptr<ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
IN_PROC_BROWSER_TEST_F(BtmWebAuthnBrowserTest,
WebAuthnAssertion_ConfirmWCOCallback) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL initial_url = TestServer()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), initial_url));
const GURL bounce_url = TestServer()->GetURL(authn_hostname, "/title1.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), bounce_url));
AccessCookieViaJSIn(GetActiveWebContents(),
GetActiveWebContents()->GetPrimaryMainFrame());
GetWebAuthnAssertion();
const GURL final_url = TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `final_url`.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), final_url));
std::vector<std::string> expected_log = {
"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)",
"WebAuthnAssertionRequestSucceeded(b.test/title1.html)",
"DidStartNavigation(d.test/title1.html)",
"DidFinishNavigation(d.test/title1.html)"};
if (base::FeatureList::IsEnabled(network::features::kGetCookiesOnSet)) {
expected_log.insert(
expected_log.begin() + 5,
"OnCookiesAccessed(RenderFrameHost, Read: b.test/title1.html)");
}
EXPECT_THAT(logger->log(), testing::ContainerEq(expected_log));
EndRedirectChain();
std::vector<std::string> expected_redirects;
// NOTE: The bounce detection isn't impacted (is exonerated) at this point by
// the web authn assertion.
expected_redirects.push_back(
"[1/1] a.test/title1.html -> b.test/title1.html (Write) -> "
"d.test/title1.html");
// NOTE: Due the favicon.ico temporally iffy callbacks we could expect the
// following outcome to help avoid flakiness.
expected_redirects.push_back(
"[1/1] a.test/title1.html -> b.test/title1.html (ReadWrite) -> "
"d.test/title1.html");
EXPECT_THAT(expected_redirects, Contains(redirects.front()));
}
// This test verifies that sites in a redirect chain with previous web authn
// assertions are not reported in the resulting issue when a navigation
// finishes.
IN_PROC_BROWSER_TEST_F(
BtmWebAuthnBrowserTest,
ReportRedirectorsInChain_OmitSitesWithWebAuthnAssertions) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> reports;
StartAppendingReportsTo(&reports);
// Visit initial page on a.test.
ASSERT_TRUE(NavigateToURL(web_contents,
TestServer()->GetURL("a.test", "/title1.html")));
GURL url = TestServer()->GetURL(authn_hostname, "/title1.html");
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents, url));
GetWebAuthnAssertion();
// Verify web authn assertion was recorded for `authn_hostname`, before
// proceeding.
std::optional<StateValue> state =
GetBtmState(GetBtmService(web_contents), url);
ASSERT_TRUE(state.has_value());
ASSERT_FALSE(state->user_activation_times.has_value());
ASSERT_TRUE(state->web_authn_assertion_times.has_value());
// Navigate with a click (not a redirect) to d.test, which statefully
// S-redirects to c.test and write a cookie on c.test.
ASSERT_TRUE(NavigateToURLFromRenderer(
web_contents,
TestServer()->GetURL(
"d.test", "/cross-site-with-samesite-none-cookie/c.test/title1.html"),
TestServer()->GetURL("c.test", "/title1.html")));
AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());
// Navigate without a click (i.e. by C-redirecting) to `authn_hostname` and
// write a cookie on `authn_hostname`:
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents, TestServer()->GetURL(authn_hostname, "/title1.html")));
AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// statefully S-redirects to f.test, which statefully S-redirects to g.test.
ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
web_contents,
TestServer()->GetURL("e.test",
"/cross-site-with-samesite-none-cookie/f.test/"
"cross-site-with-samesite-none-cookie/g.test/"
"title1.html"),
TestServer()->GetURL("g.test", "/title1.html")));
EndRedirectChain();
WaitOnStorage(GetBtmService(web_contents));
EXPECT_THAT(reports, ElementsAre(("a.test"), ("d.test"), ("c.test"),
("e.test, f.test")));
}
#endif // !BUILDFLAG(IS_ANDROID)
// Verifies that a successfully registered service worker is tracked as a
// storage access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
ServiceWorkerAccess_Storages) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
// Navigate to URL to set service workers. This will result in a service
// worker access from the RenderFrameHost.
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
embedded_test_server()->GetURL(
"/service_worker/create_service_worker.html")));
// Register a service worker on the current page, and await its completion.
ASSERT_EQ(true, EvalJs(GetActiveWebContents(), R"(
(async () => {
await navigator.serviceWorker.register('/service_worker/empty.js');
await navigator.serviceWorker.ready;
return true;
})();
)"));
// Navigate away from and back to the URL in scope of the registered service
// worker. This will result in a service worker access from the
// NavigationHandle.
ASSERT_TRUE(NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("/service_worker/empty.html")));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
embedded_test_server()->GetURL(
"/service_worker/create_service_worker.html")));
// Validate that the expected callbacks to WebContentsObserver were made.
EXPECT_THAT(logger->log(),
testing::IsSupersetOf({"OnServiceWorkerAccessed(RenderFrameHost: "
"127.0.0.1/service_worker/)",
"OnServiceWorkerAccessed(NavigationHandle:"
" 127.0.0.1/service_worker/)"}));
}
// TODO(crbug.com/40290702): Shared workers are not available on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SharedWorkerAccess_Storages DISABLED_SharedWorkerAccess_Storages
#else
#define MAYBE_SharedWorkerAccess_Storages SharedWorkerAccess_Storages
#endif
// Verifies that adding a shared worker to a frame is tracked as a storage
// access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
MAYBE_SharedWorkerAccess_Storages) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
// Add the WCOCallbackLogger as an observer of SharedWorkerService events.
GetActiveWebContents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetSharedWorkerService()
->AddObserver(logger);
// Navigate to URL for shared worker.
ASSERT_TRUE(NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/no-favicon.html")));
// Create and start a shared worker on the current page.
ASSERT_EQ(true,
EvalJs(GetActiveWebContents(), JsReplace(
R"(
(async () => {
const worker = await new Promise((resolve, reject) => {
const worker =
new SharedWorker("/workers/shared_fetcher_treat_as_public.js");
worker.port.addEventListener("message", () => resolve(worker));
worker.addEventListener("error", reject);
worker.port.start();
});
const messagePromise = new Promise((resolve) => {
const listener = (event) => resolve(event.data);
worker.port.addEventListener("message", listener, { once: true });
});
worker.port.postMessage($1);
const { error, ok } = await messagePromise;
if (error !== undefined) {
throw(error);
}
return ok;
})();
)",
embedded_test_server()->GetURL(
"b.test", "/cors-ok.txt"))));
// Validate that the expected callback to SharedWorkerService.Observer was
// made.
EXPECT_THAT(
logger->log(),
testing::Contains("OnSharedWorkerClientAdded(a.test/no-favicon.html)"));
// Clean up the observer to avoid a dangling ptr.
GetActiveWebContents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetSharedWorkerService()
->RemoveObserver(logger);
}
// Verifies that adding a dedicated worker to a frame is tracked as a storage
// access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
DedicatedWorkerAccess_Storages) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
// Add the WCOCallbackLogger as an observer of DedicatedWorkerService events.
GetActiveWebContents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetDedicatedWorkerService()
->AddObserver(logger);
// Navigate to URL for dedicated worker.
ASSERT_TRUE(NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/no-favicon.html")));
// Create and start a dedicated worker on the current page.
ASSERT_EQ(true,
EvalJs(GetActiveWebContents(), JsReplace(
R"(
(async () => {
const worker = new Worker("/workers/fetcher_treat_as_public.js");
const messagePromise = new Promise((resolve) => {
const listener = (event) => resolve(event.data);
worker.addEventListener("message", listener, { once: true });
});
worker.postMessage($1);
const { error, ok } = await messagePromise;
if (error !== undefined) {
throw(error);
}
return ok;
})();
)",
embedded_test_server()->GetURL(
"b.test", "/cors-ok.txt"))));
// Validate that the expected callback to DedicatedWorkerService.Observer was
// made.
EXPECT_THAT(
logger->log(),
testing::Contains("OnDedicatedWorkerCreated(a.test/no-favicon.html)"));
// Clean up the observer to avoid a dangling ptr.
GetActiveWebContents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetDedicatedWorkerService()
->RemoveObserver(logger);
}
// Tests that currently only work consistently when the trigger is (any) bounce.
// TODO(crbug.com/336161248) Make these tests use stateful bounces.
class BtmBounceTriggerBrowserTest : public BtmBounceDetectorBrowserTest {
protected:
BtmBounceTriggerBrowserTest() {
enabled_features_.push_back(
{features::kBtm, {{"triggering_action", "bounce"}}});
}
void SetUpOnMainThread() override {
BtmBounceDetectorBrowserTest::SetUpOnMainThread();
// BTM will only record bounces if 3PCs are blocked.
browser_client().SetBlockThirdPartyCookiesByDefault(true);
WebContents* web_contents = GetActiveWebContents();
ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
}
};
// Verifies that a HTTP 204 (No Content) response is treated like a bounce.
IN_PROC_BROWSER_TEST_F(BtmBounceTriggerBrowserTest, NoContent) {
WebContents* web_contents = GetActiveWebContents();
GURL committed_url = embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, committed_url));
BtmRedirectChainObserver observer(
BtmService::Get(web_contents->GetBrowserContext()), committed_url);
GURL nocontent_url = embedded_test_server()->GetURL("b.test", "/nocontent");
ASSERT_TRUE(NavigateToURL(web_contents, nocontent_url, committed_url));
observer.Wait();
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
}
class BtmThrottlingBrowserTest : public BtmBounceDetectorBrowserTest {
public:
void SetUpOnMainThread() override {
BtmBounceDetectorBrowserTest::SetUpOnMainThread();
BtmWebContentsObserver::FromWebContents(GetActiveWebContents())
->SetClockForTesting(&test_clock_);
}
base::SimpleTestClock test_clock_;
};
IN_PROC_BROWSER_TEST_F(BtmThrottlingBrowserTest,
InteractionRecording_Throttled) {
WebContents* web_contents = GetActiveWebContents();
const base::Time start_time = test_clock_.Now();
// Record user activation on a.test.
const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, url));
SimulateMouseClick();
// Verify the interaction was recorded in the BTM DB.
std::optional<StateValue> state =
GetBtmState(GetBtmService(web_contents), url);
ASSERT_THAT(state->user_activation_times,
testing::Optional(testing::Pair(start_time, start_time)));
// Click again, just before kBtmTimestampUpdateInterval elapses.
test_clock_.Advance(kBtmTimestampUpdateInterval - base::Seconds(1));
SimulateMouseClick();
// Verify the second interaction was NOT recorded, due to throttling.
state = GetBtmState(GetBtmService(web_contents), url);
ASSERT_THAT(state->user_activation_times,
testing::Optional(testing::Pair(start_time, start_time)));
// Click a third time, after kBtmTimestampUpdateInterval has passed since the
// first click.
test_clock_.Advance(base::Seconds(1));
SimulateMouseClick();
// Verify the third interaction WAS recorded.
state = GetBtmState(GetBtmService(web_contents), url);
ASSERT_THAT(state->user_activation_times,
testing::Optional(testing::Pair(
start_time, start_time + kBtmTimestampUpdateInterval)));
}
IN_PROC_BROWSER_TEST_F(BtmThrottlingBrowserTest,
InteractionRecording_NotThrottled_AfterRefresh) {
WebContents* web_contents = GetActiveWebContents();
const base::Time start_time = test_clock_.Now();
// Record user activation on a.test.
const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, url));
SimulateMouseClick();
// Verify the interaction was recorded in the BTM DB.
std::optional<StateValue> state =
GetBtmState(GetBtmService(web_contents), url);
ASSERT_THAT(state->user_activation_times,
testing::Optional(testing::Pair(start_time, start_time)));
// Navigate to a new page and click, only a second after the previous click.
test_clock_.Advance(base::Seconds(1));
const GURL url2 = embedded_test_server()->GetURL("b.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, url2));
SimulateMouseClick();
// Verify the second interaction was also recorded (not throttled).
state = GetBtmState(GetBtmService(web_contents), url2);
ASSERT_THAT(state->user_activation_times,
testing::Optional(testing::Pair(start_time + base::Seconds(1),
start_time + base::Seconds(1))));
}
class AllSitesFollowingFirstPartyTest : public ContentBrowserTest {
public:
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("*", "127.0.0.1");
first_party_url_ = embedded_test_server()->GetURL("a.test", "/title1.html");
third_party_url_ = embedded_test_server()->GetURL("b.test", "/title1.html");
other_url_ = embedded_test_server()->GetURL("c.test", "/title1.html");
}
WebContents* GetActiveWebContents() { return shell()->web_contents(); }
protected:
GURL first_party_url_;
GURL third_party_url_;
GURL other_url_;
};
IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest,
SiteFollowingFirstPartyIncluded) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), first_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty(
GetActiveWebContents(), first_party_url_),
testing::ElementsAre(GetSiteForBtm(third_party_url_)));
}
IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest,
SiteNotFollowingFirstPartyNotIncluded) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), first_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty(
GetActiveWebContents(), first_party_url_),
testing::ElementsAre(GetSiteForBtm(third_party_url_)));
}
IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, MultipleSitesIncluded) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), first_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), first_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty(
GetActiveWebContents(), first_party_url_),
testing::ElementsAre(GetSiteForBtm(third_party_url_),
GetSiteForBtm(other_url_)));
}
IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest,
NoFirstParty_NothingIncluded) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty(
GetActiveWebContents(), first_party_url_),
testing::IsEmpty());
}
IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest,
NothingAfterFirstParty_NothingIncluded) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), other_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), third_party_url_));
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), first_party_url_));
EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty(
GetActiveWebContents(), first_party_url_),
testing::IsEmpty());
}
class BtmPrivacySandboxDataPreservationTest : public ContentBrowserTest {
public:
BtmPrivacySandboxDataPreservationTest()
: embedded_https_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
enabled_features.emplace_back(features::kPrivacySandboxAdsAPIsOverride);
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
embedded_https_test_server_.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("content/test/data")));
RegisterTrustTokenTestHandler(&trust_token_request_handler_);
embedded_https_test_server_.SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(embedded_https_test_server_.Start());
browser_client_.emplace();
browser_client().SetBlockThirdPartyCookiesByDefault(true);
WebContents* web_contents = GetActiveWebContents();
ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
}
WebContents* GetActiveWebContents() { return shell()->web_contents(); }
base::expected<AttributionData, std::string> WaitForAttributionData() {
WebContents* web_contents = GetActiveWebContents();
AttributionDataModel* model = web_contents->GetBrowserContext()
->GetDefaultStoragePartition()
->GetAttributionDataModel();
if (!model) {
return base::unexpected("null attribution data model");
}
// Poll until data appears, failing if action_timeout() passes
base::Time deadline = base::Time::Now() + TestTimeouts::action_timeout();
while (base::Time::Now() < deadline) {
base::test::TestFuture<AttributionData> future;
model->GetAllDataKeys(future.GetCallback());
AttributionData data = future.Get();
if (!data.empty()) {
return data;
}
Sleep(TestTimeouts::tiny_timeout());
}
return base::unexpected("timed out waiting for attribution data");
}
// TODO: crbug.com/1509946 - When embedded_https_test_server() is added to
// AndroidBrowserTest, switch to using
// PlatformBrowserTest::embedded_https_test_server() and delete this.
net::EmbeddedTestServer embedded_https_test_server_;
TpcBlockingBrowserClient& browser_client() { return browser_client_->impl(); }
protected:
base::test::ScopedFeatureList scoped_feature_list_;
private:
static void Sleep(base::TimeDelta delay) {
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), delay);
run_loop.Run();
}
void RegisterTrustTokenTestHandler(
network::test::TrustTokenRequestHandler* handler) {
embedded_https_test_server_.RegisterRequestHandler(
base::BindLambdaForTesting(
[handler, this](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url != "/issue") {
return nullptr;
}
if (!base::Contains(request.headers, "Sec-Private-State-Token") ||
!base::Contains(request.headers,
"Sec-Private-State-Token-Crypto-Version")) {
return MakeTrustTokenFailureResponse();
}
std::optional<std::string> operation_result =
handler->Issue(request.headers.at("Sec-Private-State-Token"));
if (!operation_result) {
return MakeTrustTokenFailureResponse();
}
return MakeTrustTokenResponse(*operation_result);
}));
}
std::unique_ptr<net::test_server::HttpResponse>
MakeTrustTokenFailureResponse() {
// No need to report a failure HTTP code here: returning a vanilla OK should
// fail the Trust Tokens operation client-side.
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
return response;
}
// Constructs and returns an HTTP response bearing the given base64-encoded
// Trust Tokens issuance or redemption protocol response message.
std::unique_ptr<net::test_server::HttpResponse> MakeTrustTokenResponse(
std::string_view contents) {
std::string temp;
CHECK(base::Base64Decode(contents, &temp));
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("Sec-Private-State-Token", std::string(contents));
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
return response;
}
network::test::TrustTokenRequestHandler trust_token_request_handler_;
std::optional<ContentBrowserTestTpcBlockingBrowserClient> browser_client_;
};
IN_PROC_BROWSER_TEST_F(BtmPrivacySandboxDataPreservationTest,
DontClearAttributionReportingApiData) {
WebContents* web_contents = GetActiveWebContents();
GURL toplevel_url =
embedded_https_test_server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(web_contents, toplevel_url));
// Create image that registers an attribution source.
GURL attribution_url = embedded_https_test_server_.GetURL(
"b.test", "/attribution_reporting/register_source_headers.html");
ASSERT_TRUE(ExecJs(web_contents, JsReplace(
R"(
let img = document.createElement('img');
img.attributionSrc = $1;
document.body.appendChild(img);)",
attribution_url)));
// Wait for the AttributionDataModel to show that source.
ASSERT_OK_AND_ASSIGN(AttributionData data, WaitForAttributionData());
ASSERT_THAT(GetOrigins(data),
ElementsAre(url::Origin::Create(attribution_url)));
// Make the attribution site eligible for BTM deletion.
BtmServiceImpl* btm_service =
BtmServiceImpl::Get(web_contents->GetBrowserContext());
ASSERT_TRUE(btm_service != nullptr);
base::test::TestFuture<void> record_bounce;
btm_service->storage()
->AsyncCall(&BtmStorage::RecordBounce)
.WithArgs(attribution_url, base::Time::Now())
.Then(record_bounce.GetCallback());
ASSERT_TRUE(record_bounce.Wait());
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
btm_service->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
EXPECT_THAT(deleted_sites.Get(), ElementsAre(GetSiteForBtm(attribution_url)));
base::test::TestFuture<AttributionData> post_deletion_data;
web_contents->GetBrowserContext()
->GetDefaultStoragePartition()
->GetAttributionDataModel()
->GetAllDataKeys(post_deletion_data.GetCallback());
// Confirm the attribution data was not deleted.
EXPECT_THAT(GetOrigins(post_deletion_data.Get()),
ElementsAre(url::Origin::Create(attribution_url)));
}
namespace {
class SiteStorage {
public:
constexpr SiteStorage() = default;
virtual base::expected<std::string, std::string> ReadValue(
RenderFrameHost* frame) const = 0;
virtual testing::AssertionResult WriteValue(RenderFrameHost* frame,
std::string_view value,
bool partitioned) const = 0;
virtual std::string_view name() const = 0;
};
class CookieStorage : public SiteStorage {
base::expected<std::string, std::string> ReadValue(
RenderFrameHost* frame) const override {
EvalJsResult result =
EvalJs(frame, "document.cookie", EXECUTE_SCRIPT_NO_USER_GESTURE);
if (!result.is_ok()) {
return base::unexpected(result.ExtractError());
}
return base::ok(result.ExtractString());
}
testing::AssertionResult WriteValue(RenderFrameHost* frame,
std::string_view cookie,
bool partitioned) const override {
std::string value(cookie);
if (partitioned) {
value += ";Secure;Partitioned;SameSite=None";
}
FrameCookieAccessObserver obs(WebContents::FromRenderFrameHost(frame),
frame, CookieOperation::kChange);
testing::AssertionResult result =
ExecJs(frame, JsReplace("document.cookie = $1;", value),
EXECUTE_SCRIPT_NO_USER_GESTURE);
if (result) {
obs.Wait();
}
return result;
}
std::string_view name() const override { return "CookieStorage"; }
};
class LocalStorage : public SiteStorage {
base::expected<std::string, std::string> ReadValue(
RenderFrameHost* frame) const override {
EvalJsResult result = EvalJs(frame, "localStorage.getItem('value')",
EXECUTE_SCRIPT_NO_USER_GESTURE);
if (!result.is_ok()) {
return base::unexpected(result.ExtractError());
}
if (result == base::Value()) {
return base::ok("");
}
return base::ok(result.ExtractString());
}
testing::AssertionResult WriteValue(RenderFrameHost* frame,
std::string_view value,
bool partitioned) const override {
return ExecJs(frame, JsReplace("localStorage.setItem('value', $1);", value),
EXECUTE_SCRIPT_NO_USER_GESTURE);
}
std::string_view name() const override { return "LocalStorage"; }
};
void PrintTo(const SiteStorage* storage, std::ostream* os) {
*os << storage->name();
}
static constexpr CookieStorage kCookieStorage;
static constexpr LocalStorage kLocalStorage;
} // namespace
class BtmDataDeletionBrowserTest
: public BtmBounceDetectorBrowserTest,
public testing::WithParamInterface<const SiteStorage*> {
public:
void SetUpOnMainThread() override {
BtmBounceDetectorBrowserTest::SetUpOnMainThread();
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server_.AddDefaultHandlers(kContentTestDataDir);
ASSERT_TRUE(https_server_.Start());
browser_client().SetBlockThirdPartyCookiesByDefault(true);
WebContents* web_contents = GetActiveWebContents();
ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
}
const net::EmbeddedTestServer& https_server() const { return https_server_; }
[[nodiscard]] testing::AssertionResult WriteToPartitionedStorage(
std::string_view first_party_hostname,
std::string_view third_party_hostname,
std::string_view value) {
WebContents* web_contents = GetActiveWebContents();
if (!NavigateToURL(web_contents,
https_server().GetURL(first_party_hostname,
"/page_with_blank_iframe.html"))) {
return testing::AssertionFailure() << "Failed to navigate top-level";
}
const std::string_view kIframeId = "test_iframe";
if (!NavigateIframeToURL(
web_contents, kIframeId,
https_server().GetURL(third_party_hostname, "/title1.html"))) {
return testing::AssertionFailure() << "Failed to navigate iframe";
}
RenderFrameHost* iframe = ChildFrameAt(web_contents, 0);
if (!iframe) {
return testing::AssertionFailure() << "Child frame not found";
}
return WriteValue(iframe, value, /*partitioned=*/true);
}
[[nodiscard]] base::expected<std::string, std::string>
ReadFromPartitionedStorage(std::string_view first_party_hostname,
std::string_view third_party_hostname) {
WebContents* web_contents = GetActiveWebContents();
if (!NavigateToURL(web_contents,
https_server().GetURL(first_party_hostname,
"/page_with_blank_iframe.html"))) {
return base::unexpected("Failed to navigate top-level");
}
const std::string_view kIframeId = "test_iframe";
if (!NavigateIframeToURL(
web_contents, kIframeId,
https_server().GetURL(third_party_hostname, "/title1.html"))) {
return base::unexpected("Failed to navigate iframe");
}
RenderFrameHost* iframe = ChildFrameAt(web_contents, 0);
if (!iframe) {
return base::unexpected("iframe not found");
}
return ReadValue(iframe);
}
[[nodiscard]] base::expected<std::string, std::string> ReadFromStorage(
std::string_view hostname) {
WebContents* web_contents = GetActiveWebContents();
if (!NavigateToURL(web_contents,
https_server().GetURL(hostname, "/title1.html"))) {
return base::unexpected("Failed to navigate");
}
return ReadValue(web_contents);
}
[[nodiscard]] testing::AssertionResult WriteToStorage(
std::string_view hostname,
std::string_view value) {
WebContents* web_contents = GetActiveWebContents();
if (!NavigateToURL(web_contents,
https_server().GetURL(hostname, "/title1.html"))) {
return testing::AssertionFailure() << "Failed to navigate";
}
return WriteValue(web_contents, value);
}
// Navigates to host1, then performs a stateful bounce on host2 to host3.
[[nodiscard]] testing::AssertionResult DoStatefulBounce(
std::string_view host1,
std::string_view host2,
std::string_view host3) {
WebContents* web_contents = GetActiveWebContents();
if (!NavigateToURL(web_contents,
https_server().GetURL(host1, "/title1.html"))) {
return testing::AssertionFailure() << "Failed to navigate to " << host1;
}
if (!NavigateToURLFromRenderer(
web_contents, https_server().GetURL(host2, "/title1.html"))) {
return testing::AssertionFailure() << "Failed to navigate to " << host2;
}
testing::AssertionResult result = WriteValue(web_contents, "bounce=yes");
if (!result) {
return result;
}
if (!NavigateToURLFromRendererWithoutUserGesture(
web_contents, https_server().GetURL(host3, "/title1.html"))) {
return testing::AssertionFailure() << "Failed to navigate to " << host3;
}
EndRedirectChain();
return testing::AssertionSuccess();
}
private:
const SiteStorage* storage() { return GetParam(); }
[[nodiscard]] base::expected<std::string, std::string> ReadValue(
const ToRenderFrameHost& frame) {
return storage()->ReadValue(frame.render_frame_host());
}
[[nodiscard]] testing::AssertionResult WriteValue(
const ToRenderFrameHost& frame,
std::string_view value,
bool partitioned = false) {
return storage()->WriteValue(frame.render_frame_host(), value, partitioned);
}
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DontDeleteIfTpcsEnabled) {
// Do not block third-party cookies by default. This should make it such that
// BTM deletion does not run.
browser_client().SetBlockThirdPartyCookiesByDefault(false);
WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
web_contents));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Confirm unpartitioned storage was written on b.test.
EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
// Navigate away from b.test since BTM won't delete its state while loaded.
ASSERT_TRUE(NavigateToURL(web_contents,
https_server().GetURL("a.test", "/title1.html")));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
// Confirm that nothing was deleted.
EXPECT_THAT(deleted_sites.Get(), IsEmpty());
// Confirm b.test storage has not changed.
EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteDomain) {
WebContents* web_contents = GetActiveWebContents();
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Confirm unpartitioned storage was written on b.test.
EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
// Navigate away from b.test since BTM won't delete its state while loaded.
ASSERT_TRUE(NavigateToURL(web_contents,
https_server().GetURL("a.test", "/title1.html")));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm b.test storage was deleted.
EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs(""));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DontDeleteOtherDomains) {
WebContents* web_contents = GetActiveWebContents();
// Set storage on a.test
ASSERT_TRUE(WriteToStorage("a.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromStorage("a.test"), base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm a.test storage was NOT deleted.
EXPECT_THAT(ReadFromStorage("a.test"), base::test::ValueIs("foo=bar"));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest,
DontDeleteDomainWhenPartitioned) {
WebContents* web_contents = GetActiveWebContents();
// Set storage on b.test embedded in a.test.
ASSERT_TRUE(WriteToPartitionedStorage("a.test", "b.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromPartitionedStorage("a.test", "b.test"),
base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm partitioned storage was NOT deleted.
EXPECT_THAT(ReadFromPartitionedStorage("a.test", "b.test"),
base::test::ValueIs("foo=bar"));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteSubdomains) {
WebContents* web_contents = GetActiveWebContents();
// Set storage on sub.b.test
ASSERT_TRUE(WriteToStorage("sub.b.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromStorage("sub.b.test"), base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm sub.b.test storage was deleted.
EXPECT_THAT(ReadFromStorage("sub.b.test"), base::test::ValueIs(""));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteEmbedded3Ps) {
WebContents* web_contents = GetActiveWebContents();
// Set storage on a.test embedded in b.test.
ASSERT_TRUE(WriteToPartitionedStorage("b.test", "a.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromPartitionedStorage("b.test", "a.test"),
base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm partitioned a.test storage was deleted.
EXPECT_THAT(ReadFromPartitionedStorage("b.test", "a.test"),
base::test::ValueIs(""));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest,
DeleteEmbedded3Ps_Subdomain) {
WebContents* web_contents = GetActiveWebContents();
// Set storage on a.test embedded in sub.b.test.
ASSERT_TRUE(WriteToPartitionedStorage("sub.b.test", "a.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromPartitionedStorage("sub.b.test", "a.test"),
base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on b.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
// Confirm partitioned a.test storage was deleted.
EXPECT_THAT(ReadFromPartitionedStorage("sub.b.test", "a.test"),
base::test::ValueIs(""));
}
IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteEmbedded1Ps) {
content::WebContents* web_contents = GetActiveWebContents();
// Set storage on a.test embedded in another a.test.
ASSERT_TRUE(WriteToPartitionedStorage("a.test", "a.test", "foo=bar"));
// Confirm written.
EXPECT_THAT(ReadFromPartitionedStorage("a.test", "a.test"),
base::test::ValueIs("foo=bar"));
// Perform a stateful bounce on a.test to make it eligible for deletion.
ASSERT_TRUE(DoStatefulBounce("b.test", "a.test", "c.test"));
// Trigger BTM deletion.
base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
BtmService::Get(web_contents->GetBrowserContext())
->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
ASSERT_THAT(deleted_sites.Get(), ElementsAre("a.test"));
// Confirm partitioned a.test storage was deleted.
EXPECT_THAT(ReadFromPartitionedStorage("a.test", "a.test"),
base::test::ValueIs(""));
}
INSTANTIATE_TEST_SUITE_P(All,
BtmDataDeletionBrowserTest,
::testing::Values(&kCookieStorage, &kLocalStorage));
class BtmBounceDetectorBFCacheTest : public BtmBounceDetectorBrowserTest,
public testing::WithParamInterface<bool> {
public:
void SetUp() override {
if (IsBFCacheEnabled() &&
!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
GTEST_SKIP() << "BFCache disabled";
}
BtmBounceDetectorBrowserTest::SetUp();
}
bool IsBFCacheEnabled() const { return GetParam(); }
void SetUpOnMainThread() override {
if (!IsBFCacheEnabled()) {
DisableBackForwardCacheForTesting(
GetActiveWebContents(),
BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
}
BtmBounceDetectorBrowserTest::SetUpOnMainThread();
}
};
// Confirm that BTM records a bounce, even if the user immediately navigates
// away.
// TODO(https://crbug.com/425717555): Very flaky if BF Cache is disabled.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_LateCookieAccessTest DISABLED_LateCookieAccessTest
#else
#define MAYBE_LateCookieAccessTest LateCookieAccessTest
#endif
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
MAYBE_LateCookieAccessTest) {
const GURL bounce_url =
embedded_test_server()->GetURL("b.test", "/empty.html");
const GURL final_url =
embedded_test_server()->GetURL("c.test", "/empty.html");
WebContents* const web_contents = GetActiveWebContents();
RedirectChainDetector* wco =
RedirectChainDetector::FromWebContents(web_contents);
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));
ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'bounce=true';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
URLCookieAccessObserver cookie_observer(web_contents, final_url,
CookieOperation::kChange);
ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'final=yes';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
// Since cookies are reported serially, both cookie writes should have been
// reported by now.
const BtmRedirectContext& context = wco->CommittedRedirectContext();
ASSERT_EQ(context.size(), 1u);
const BtmRedirectInfo& redirect = context[0];
EXPECT_EQ(redirect.redirector_url, bounce_url);
// A request to /favicon.ico may cause a cookie read in addition to the write
// we explicitly performed.
EXPECT_THAT(
redirect.access_type,
testing::AnyOf(BtmDataAccessType::kWrite, BtmDataAccessType::kReadWrite));
}
// Confirm that WCO::OnCookiesAccessed() is always called even if the user
// immediately navigates away.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest, CookieAccessReported) {
const GURL url1 = embedded_test_server()->GetURL("a.test", "/empty.html");
const GURL url2 = embedded_test_server()->GetURL("b.test", "/empty.html");
const GURL url3 = embedded_test_server()->GetURL("c.test", "/empty.html");
WebContents* const web_contents = GetActiveWebContents();
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
ASSERT_TRUE(NavigateToURL(web_contents, url1));
ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'initial=true';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
ASSERT_TRUE(NavigateToURL(web_contents, url2));
ASSERT_TRUE(NavigateToURL(web_contents, url3));
URLCookieAccessObserver cookie_observer(web_contents, url3,
CookieOperation::kChange);
ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'final=yes';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
EXPECT_THAT(
logger->log(),
testing::Contains(
"OnCookiesAccessed(RenderFrameHost, Change: a.test/empty.html)"));
}
// Confirm that BTM records an interaction, even if the user immediately
// navigates away.
//
// TODO: crbug.com/376625002 - After moving to //content, this test was flaky
// because the navigation to final_url unexpectedly sometimes has a user
// gesture. Because there's no indication of a fault in BTM, we disabled this
// test to get the move done, but we should try to fix and re-enable it.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
DISABLED_LateInteractionTest) {
const GURL bounce_url =
embedded_test_server()->GetURL("b.test", "/empty.html");
const GURL final_url =
embedded_test_server()->GetURL("c.test", "/empty.html");
WebContents* const web_contents = GetActiveWebContents();
RedirectChainDetector* wco =
RedirectChainDetector::FromWebContents(web_contents);
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));
ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
::content::SimulateMouseClick(web_contents, 0,
blink::WebMouseEvent::Button::kLeft);
// Consume the transient user activation so the next navigation is not
// considered to be user-initiated and will be judged a bounce.
if (EvalJs(web_contents, "!open('about:blank')",
EXECUTE_SCRIPT_NO_USER_GESTURE)
.ExtractBool()) {
// Due to a race condition, the open() call might be executed before the
// click is processed, causing open() to fail and leaving the window with
// transient user activation. In such a case, just skip the test. (If we
// used UserActivationObserver::Wait() here, it would defeat the purpose of
// this test, which is to verify that BTM sees the interaction even if the
// test doesn't wait for it.)
GTEST_SKIP();
}
ASSERT_FALSE(
web_contents->GetPrimaryMainFrame()->HasTransientUserActivation());
ASSERT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
UserActivationObserver interaction_observer(
web_contents, web_contents->GetPrimaryMainFrame());
::content::SimulateMouseClick(web_contents, 0,
blink::WebMouseEvent::Button::kLeft);
interaction_observer.Wait();
const BtmRedirectContext& context = wco->CommittedRedirectContext();
ASSERT_EQ(context.size(), 1u);
const BtmRedirectInfo& redirect = context[0];
EXPECT_EQ(redirect.redirector_url, bounce_url);
EXPECT_THAT(redirect.has_sticky_activation, true);
}
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest, IsOrWasInPrimaryPage) {
WebContents* const web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));
RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
EXPECT_TRUE(IsInPrimaryPage(*rfh));
EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
const GlobalRenderFrameHostId rfh_id = rfh->GetGlobalId();
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));
// Attempt to get a pointer to the RFH of the a.test page, although
rfh = RenderFrameHost::FromID(rfh_id);
if (IsBFCacheEnabled()) {
// If the bfcache is enabled, the RFH should be in the cache.
ASSERT_TRUE(rfh);
EXPECT_TRUE(rfh->IsInLifecycleState(
RenderFrameHost::LifecycleState::kInBackForwardCache));
// The page is no longer primary, but it used to be:
EXPECT_FALSE(IsInPrimaryPage(*rfh));
EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
} else {
// If the bfcache is disabled, the RFH may or may not be in memory. If it
// still is, it's only because it's pending deletion.
if (rfh) {
EXPECT_TRUE(rfh->IsInLifecycleState(
RenderFrameHost::LifecycleState::kPendingDeletion));
// The page is no longer primary, but it used to be:
EXPECT_FALSE(IsInPrimaryPage(*rfh));
EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
}
}
}
// For waiting until prerendering starts.
class PrerenderingObserver : public WebContentsObserver {
public:
explicit PrerenderingObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
void Wait() { run_loop_.Run(); }
GlobalRenderFrameHostId rfh_id() const {
CHECK(rfh_id_.has_value());
return rfh_id_.value();
}
private:
base::RunLoop run_loop_;
std::optional<GlobalRenderFrameHostId> rfh_id_;
void RenderFrameCreated(RenderFrameHost* render_frame_host) override;
};
void PrerenderingObserver::RenderFrameCreated(
RenderFrameHost* render_frame_host) {
if (render_frame_host->IsInLifecycleState(
RenderFrameHost::LifecycleState::kPrerendering)) {
rfh_id_ = render_frame_host->GetGlobalId();
run_loop_.Quit();
}
}
// Confirm that IsOrWasInPrimaryPage() returns false for prerendered pages that
// are never activated.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
PrerenderedPagesAreNotPrimary) {
WebContents* const web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/empty.html?primary")));
PrerenderingObserver observer(web_contents);
ASSERT_TRUE(ExecJs(web_contents, R"(
const elt = document.createElement('script');
elt.setAttribute('type', 'speculationrules');
elt.textContent = JSON.stringify({
prerender: [{'urls': ['empty.html?prerendered']}]
}); document.body.appendChild(elt);
)"));
observer.Wait();
ASSERT_FALSE(testing::Test::HasFailure())
<< "Failed waiting for prerendering";
RenderFrameHost* rfh = RenderFrameHost::FromID(observer.rfh_id());
ASSERT_TRUE(rfh);
EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));
// Navigating to another site may trigger destruction of the frame.
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));
rfh = RenderFrameHost::FromID(observer.rfh_id());
if (rfh) {
// Even if it's still in memory, it was never primary.
EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));
}
}
// Confirm that IsOrWasInPrimaryPage() returns true for prerendered pages that
// get activated.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
PrerenderedPagesCanBecomePrimary) {
WebContents* const web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/empty.html?primary")));
PrerenderingObserver observer(web_contents);
ASSERT_TRUE(ExecJs(web_contents, R"(
const elt = document.createElement('script');
elt.setAttribute('type', 'speculationrules');
elt.textContent = JSON.stringify({
prerender: [{'urls': ['empty.html?prerendered']}]
});
document.body.appendChild(elt);
)"));
observer.Wait();
ASSERT_FALSE(testing::Test::HasFailure())
<< "Failed waiting for prerendering";
RenderFrameHost* rfh = RenderFrameHost::FromID(observer.rfh_id());
ASSERT_TRUE(rfh);
EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));
// Navigate to the prerendered page.
ASSERT_TRUE(NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("a.test", "/empty.html?prerendered")));
// Navigate to another page, so the prerendered page is no longer active.
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));
rfh = RenderFrameHost::FromID(observer.rfh_id());
if (rfh) {
EXPECT_FALSE(IsInPrimaryPage(*rfh));
EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
}
}
INSTANTIATE_TEST_SUITE_P(All, BtmBounceDetectorBFCacheTest, ::testing::Bool());
} // namespace content