blob: be3e6ae3ade0ce75884ce5fd3c5b5ef97ab15375 [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 "chrome/browser/dips/dips_bounce_detector.h"
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/dips/dips_service.h"
#include "chrome/browser/dips/dips_service_factory.h"
#include "chrome/browser/dips/dips_test_utils.h"
#include "chrome/browser/dips/dips_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.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/test_devtools_protocol_client.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/metrics_proto/ukm/source.pb.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/test/base/android/android_browser_test.h"
#else
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "device/fido/virtual_fido_device_factory.h"
#endif // BUILDFLAG(IS_ANDROID)
using base::Bucket;
using content::CookieAccessDetails;
using content::NavigationHandle;
using content::WebContents;
using testing::Contains;
using testing::ElementsAre;
using testing::Eq;
using testing::Gt;
using testing::IsEmpty;
using testing::Pair;
using ukm::builders::DIPS_Redirect;
namespace {
using StorageType =
content_settings::mojom::ContentSettingsManager::StorageType;
inline const std::string StorageTypeTestName(const StorageType& type) {
switch (type) {
case StorageType::DATABASE:
return "Database";
case StorageType::LOCAL_STORAGE:
return "LocalStorage";
case StorageType::SESSION_STORAGE:
return "SessionStorage";
case StorageType::FILE_SYSTEM:
return "FileSystem";
case StorageType::INDEXED_DB:
return "IndexedDB";
case StorageType::CACHE:
return "Cache";
case StorageType::WEB_LOCKS:
return "WebLocks";
}
}
// 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 DIPSRedirectInfo& redirect,
const DIPSRedirectChainInfo& 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.url).c_str(),
std::string(SiteDataAccessTypeToString(redirect.access_type)).c_str(),
FormatURL(chain.final_url).c_str()));
}
void AppendRedirects(std::vector<std::string>* vec,
std::vector<DIPSRedirectInfoPtr> redirects,
DIPSRedirectChainInfoPtr 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<base::StringPiece>(sites.begin(), sites.end()), ", "));
}
} // namespace
// Keeps a log of DidStartNavigation, OnCookiesAccessed, and DidFinishNavigation
// executions.
class WCOCallbackLogger
: public content_settings::PageSpecificContentSettings::SiteDataObserver,
public content::WebContentsObserver,
public content::WebContentsUserData<WCOCallbackLogger> {
public:
WCOCallbackLogger(const WCOCallbackLogger&) = delete;
WCOCallbackLogger& operator=(const WCOCallbackLogger&) = delete;
const std::vector<std::string>& log() const { return log_; }
private:
explicit WCOCallbackLogger(content::WebContents* web_contents);
// So WebContentsUserData::CreateForWebContents() can call the constructor.
friend class content::WebContentsUserData<WCOCallbackLogger>;
// Start WebContentsObserver overrides:
void DidStartNavigation(NavigationHandle* navigation_handle) override;
void OnCookiesAccessed(content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) override;
void OnCookiesAccessed(NavigationHandle* navigation_handle,
const content::CookieAccessDetails& details) override;
void DidFinishNavigation(NavigationHandle* navigation_handle) override;
void WebAuthnAssertionRequestSucceeded(
content::RenderFrameHost* render_frame_host) override;
// End WebContentsObserver overrides.
// Start SiteDataObserver overrides:
void OnSiteDataAccessed(
const content_settings::AccessDetails& access_details) override;
void OnStatefulBounceDetected() override;
// End SiteDataObserver overrides.
std::vector<std::string> log_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WCOCallbackLogger::WCOCallbackLogger(content::WebContents* web_contents)
: content_settings::PageSpecificContentSettings::SiteDataObserver(
web_contents),
content::WebContentsObserver(web_contents),
content::WebContentsUserData<WCOCallbackLogger>(*web_contents) {}
void WCOCallbackLogger::DidStartNavigation(
NavigationHandle* navigation_handle) {
log_.push_back(
base::StringPrintf("DidStartNavigation(%s)",
FormatURL(navigation_handle->GetURL()).c_str()));
}
void WCOCallbackLogger::OnCookiesAccessed(
content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) {
// Callbacks for favicons are ignored only in testing logs because their
// ordering is variable and would cause flakiness
if (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 content::CookieAccessDetails& details) {
log_.push_back(base::StringPrintf(
"OnCookiesAccessed(NavigationHandle, %s: %s)",
details.type == CookieOperation::kChange ? "Change" : "Read",
FormatURL(details.url).c_str()));
}
void WCOCallbackLogger::DidFinishNavigation(
NavigationHandle* navigation_handle) {
if (!IsInPrimaryPage(navigation_handle)) {
return;
}
// Android testing produces callbacks for a finished navigation to "blank" at
// the beginning of a test. These should be ignored here.
if (FormatURL(navigation_handle->GetURL()) == "blank" ||
navigation_handle->GetPreviousPrimaryMainFrameURL().is_empty()) {
return;
}
log_.push_back(
base::StringPrintf("DidFinishNavigation(%s)",
FormatURL(navigation_handle->GetURL()).c_str()));
}
void WCOCallbackLogger::WebAuthnAssertionRequestSucceeded(
content::RenderFrameHost* render_frame_host) {
log_.push_back(base::StringPrintf(
"WebAuthnAssertionRequestSucceeded(%s)",
FormatURL(render_frame_host->GetLastCommittedURL()).c_str()));
}
inline std::string SiteDataTypeToString(
const content_settings::SiteDataType& type) {
switch (type) {
case content_settings::SiteDataType::kUnknown:
return "Unknown";
case content_settings::SiteDataType::kStorage:
return "Storage";
case content_settings::SiteDataType::kCookies:
return "Cookies";
case content_settings::SiteDataType::kServiceWorker:
return "ServiceWorker";
case content_settings::SiteDataType::kSharedWorker:
return "SharedWorker";
case content_settings::SiteDataType::kInterestGroup:
return "InterestGroup";
case content_settings::SiteDataType::kTopic:
return "Topics";
case content_settings::SiteDataType::kTrustToken:
return "TrustToken";
}
}
inline std::string AccessTypeToString(content_settings::AccessType type) {
switch (type) {
case content_settings::AccessType::kUnknown:
return "Unknown";
case content_settings::AccessType::kRead:
return "Read";
case content_settings::AccessType::kWrite:
return "Write";
}
}
void WCOCallbackLogger::OnSiteDataAccessed(
const content_settings::AccessDetails& access_details) {
// Avoids logging notification from the PSCS that are due to cookie accesses,
// in order not to impact the other cookie access notification logs from the
// `WebContentsObserver`.
if (access_details.site_data_type ==
content_settings::SiteDataType::kCookies) {
return;
}
log_.push_back(base::StringPrintf(
"OnSiteDataAccessed(AccessDetails, %s: %s: %s)",
SiteDataTypeToString(access_details.site_data_type).c_str(),
AccessTypeToString(access_details.access_type).c_str(),
FormatURL(access_details.url).c_str()));
}
void WCOCallbackLogger::OnStatefulBounceDetected() {}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WCOCallbackLogger);
class DIPSBounceDetectorBrowserTest : public PlatformBrowserTest {
protected:
DIPSBounceDetectorBrowserTest()
: prerender_test_helper_(base::BindRepeating(
&DIPSBounceDetectorBrowserTest::GetActiveWebContents,
base::Unretained(this))) {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{},
/*disabled_features=*/{
// TODO(crbug.com/1394910): Use HTTPS URLs in tests to avoid having
// to disable this feature.
features::kHttpsUpgrades,
});
}
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 {
prerender_test_helper_.SetUp(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("a.test", "127.0.0.1");
host_resolver()->AddRule("b.test", "127.0.0.1");
host_resolver()->AddRule("sub.b.test", "127.0.0.1");
host_resolver()->AddRule("c.test", "127.0.0.1");
host_resolver()->AddRule("d.test", "127.0.0.1");
host_resolver()->AddRule("e.test", "127.0.0.1");
host_resolver()->AddRule("f.test", "127.0.0.1");
host_resolver()->AddRule("g.test", "127.0.0.1");
SetUpDIPSWebContentsObserver();
}
void SetUpDIPSWebContentsObserver() {
web_contents_observer_ =
DIPSWebContentsObserver::FromWebContents(GetActiveWebContents());
}
content::WebContents* GetActiveWebContents() {
return chrome_test_utils::GetActiveWebContents(this);
}
void StartAppendingRedirectsTo(std::vector<std::string>* redirects) {
web_contents_observer_->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, redirects));
}
void StartAppendingReportsTo(std::vector<std::string>* reports) {
web_contents_observer_->SetIssueReportingCallbackForTesting(
base::BindRepeating(&AppendSitesInReport, reports));
}
void BlockUntilHelperProcessesPendingRequests() {
base::SequenceBound<DIPSStorage>* storage =
DIPSServiceFactory::GetForBrowserContext(
GetActiveWebContents()->GetBrowserContext())
->storage();
storage->FlushPostedTasksForTesting();
}
void StateForURL(const GURL& url, StateForURLCallback callback) {
DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext(
GetActiveWebContents()->GetBrowserContext());
dips_service->storage()
->AsyncCall(&DIPSStorage::Read)
.WithArgs(url)
.Then(std::move(callback));
}
absl::optional<StateValue> GetDIPSState(const GURL& url) {
absl::optional<StateValue> state;
StateForURL(url, base::BindLambdaForTesting([&](DIPSState loaded_state) {
if (loaded_state.was_loaded()) {
state = loaded_state.ToStateValue();
}
}));
BlockUntilHelperProcessesPendingRequests();
return state;
}
// Navigate to /set-cookie on `host` and wait for OnCookiesAccessed() to be
// called.
[[nodiscard]] bool NavigateToSetCookie(base::StringPiece host) {
auto* web_contents = GetActiveWebContents();
const auto url =
embedded_test_server()->GetURL(host, "/set-cookie?name=value");
URLCookieAccessObserver observer(web_contents, url,
CookieOperation::kChange);
bool success = content::NavigateToURL(web_contents, url);
if (success) {
observer.Wait();
}
return success;
}
void CreateImageAndWaitForCookieAccess(const GURL& image_url) {
WebContents* web_contents = GetActiveWebContents();
URLCookieAccessObserver observer(web_contents, image_url,
CookieOperation::kRead);
ASSERT_TRUE(content::ExecJs(web_contents,
content::JsReplace(
R"(
let img = document.createElement('img');
img.src = $1;
document.body.appendChild(img);)",
image_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
// The image must cause a cookie access, or else this will hang.
observer.Wait();
}
// Perform a browser-based navigation to terminate the current redirect chain.
// (NOTE: tests using WCOCallbackLogger must call this *after* checking the
// log, since this navigation will be logged.)
void EndRedirectChain() {
ASSERT_TRUE(content::NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/title1.html")));
}
[[nodiscard]] bool AccessStorage(content::RenderFrameHost* frame,
const StorageType& type) {
return content::ExecJs(
frame,
base::StringPrintf(kStorageAccessScript,
StorageTypeTestName(type).c_str()),
content::EXECUTE_SCRIPT_NO_USER_GESTURE,
/*world_id=*/1);
}
auto* fenced_frame_test_helper() { return &fenced_frame_test_helper_; }
auto* prerender_test_helper() { return &prerender_test_helper_; }
content::RenderFrameHost* GetIFrame() {
content::WebContents* web_contents = GetActiveWebContents();
return ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
}
content::RenderFrameHost* GetNestedIFrame() {
return ChildFrameAt(GetIFrame(), 0);
}
void NavigateNestedIFrameTo(content::RenderFrameHost* parent_frame,
const std::string& iframe_id,
const GURL& url) {
content::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(content::ExecJs(parent_frame, script,
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
load_observer.Wait();
}
void AccessCHIPSViaJSIn(content::RenderFrameHost* frame) {
FrameCookieAccessObserver observer(GetActiveWebContents(), frame,
CookieOperation::kChange);
ASSERT_TRUE(content::ExecJs(frame,
"document.cookie = '__Host-foo=bar;"
"SameSite=None;Secure;Path=/;Partitioned';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
}
const base::FilePath kChromeTestDataDir =
base::FilePath(FILE_PATH_LITERAL("chrome/test/data"));
private:
content::test::PrerenderTestHelper prerender_test_helper_;
content::test::FencedFrameTestHelper fenced_frame_test_helper_;
base::test::ScopedFeatureList scoped_feature_list_;
raw_ptr<DIPSWebContentsObserver, DanglingUntriaged> web_contents_observer_ =
nullptr;
};
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AttributeSameSiteIframesCookieClientAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AttributeSameSiteIframesCookieServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Attribute3PIframesCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = https_server.GetURL("b.test", "/title1.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Attribute3PIframesCHIPSServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
https_server.GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::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(
content::NavigateIframeToURL(GetActiveWebContents(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AttributeSameSiteNestedIframesCookieClientAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", iframe_url));
const GURL nested_iframe_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AttributeSameSiteNestedIframesCookieServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", iframe_url));
const GURL nested_iframe_url =
https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
NavigateNestedIFrameTo(GetIFrame(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Attribute3PNestedIframesCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("b.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", iframe_url));
const GURL nested_iframe_url = https_server.GetURL("c.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Attribute3PNestedIframesCHIPSServerAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server.Start());
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("b.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", 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", 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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
Attribute3PSubResourceCHIPSClientAccessTo1P) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
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", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url =
embedded_test_server()->GetURL("b.test", "/iframe_blank.html");
ASSERT_TRUE(content::NavigateIframeToURL(GetActiveWebContents(), "test",
iframe_url));
const GURL nested_iframe_url =
https_server.GetURL("c.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test", nested_iframe_url);
AccessCHIPSViaJSIn(GetNestedIFrame());
}
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
GURL image_url = https_server.GetURL("c.test", "/favicon/icon.png");
CreateImageAndWaitForCookieAccess(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(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(
redirects,
ElementsAre(("[1/1] a.test/iframe_blank.html -> a.test/iframe_blank.html "
"(Read) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DiscardFencedFrameCookieClientAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL fenced_frame_url =
embedded_test_server()->GetURL("a.test", "/fenced_frames/title2.html");
content::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(content::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(DIPSBounceDetectorBrowserTest,
DiscardFencedFrameCookieServerAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
embedded_test_server()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::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);
content::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(content::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/1454793): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
DISABLED_DiscardPrerenderedPageCookieClientAccess
#else
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
DiscardPrerenderedPageCookieClientAccess
#endif
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
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(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL prerendering_url =
embedded_test_server()->GetURL("a.test", "/title2.html");
const int host_id = prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
content::test::PrerenderHostObserver observer(*GetActiveWebContents(),
host_id);
EXPECT_FALSE(observer.was_activated());
content::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(content::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(crrev/1448453): flaky test.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
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(
content::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 int host_id = prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
observer.Wait();
content::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(content::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/1447929): 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(DIPSBounceDetectorBrowserTest,
DetectStatefulBounce_ClientRedirect_SiteDataAccess) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Navigate to the initial page, a.test.
ASSERT_TRUE(content::NavigateToURL(
GetActiveWebContents(),
embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not considered to be redirect) to b.test.
ASSERT_TRUE(content::NavigateToURLFromRenderer(
GetActiveWebContents(),
embedded_test_server()->GetURL("b.test", "/title1.html")));
EXPECT_TRUE(AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(),
StorageType::LOCAL_STORAGE));
// Navigate without a click (considered a client-redirect) to c.test.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(),
embedded_test_server()->GetURL("c.test", "/title1.html")));
EndRedirectChain();
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 DIPSBounceDetector needs to know when it's safe to
// judge an HTTP redirect as stateful (accessing cookies) or not. This test
// tries to verify that OnCookiesAccessed() is always called before
// DidFinishNavigation(), so that DIPSBounceDetector can safely perform that
// judgement in DidFinishNavigation().
//
// This test also verifies that OnCookiesAccessed() is called for URLs in the
// same order that they're visited (and that for redirects that both read and
// write cookies, OnCookiesAccessed() is called with kRead before it's called
// with kChange, although DIPSBounceDetector doesn't depend on that anymore.)
//
// If either assumption is incorrect, this test will be flaky. On 2022-04-27 I
// (rtarpine) ran this test 1000 times in 40 parallel jobs with no failures, so
// it seems robust.
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
AllCookieCallbacksBeforeNavigationFinished) {
GURL redirect_url = embedded_test_server()->GetURL(
"a.test",
"/cross-site/b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/"
"d.test/set-cookie?name=value");
GURL final_url =
embedded_test_server()->GetURL("d.test", "/set-cookie?name=value");
content::WebContents* web_contents = GetActiveWebContents();
// Set cookies on all 4 test domains
ASSERT_TRUE(NavigateToSetCookie("a.test"));
ASSERT_TRUE(NavigateToSetCookie("b.test"));
ASSERT_TRUE(NavigateToSetCookie("c.test"));
ASSERT_TRUE(NavigateToSetCookie("d.test"));
// 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(content::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(DIPSBounceDetectorBrowserTest,
IgnoreServerRedirectsInIframes) {
// We host the iframe content on an HTTPS server, because for it to write a
// cookie, the cookie needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
https_server.RegisterDefaultHandler(base::BindRepeating(
&HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
ASSERT_TRUE(https_server.Start());
const GURL root_url =
embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
const GURL redirect_url = https_server.GetURL(
"b.test", "/cross-site-with-samesite-none-cookie/c.test/title1.html");
const std::string iframe_id = "test";
content::WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
ASSERT_TRUE(content::NavigateToURL(web_contents, root_url));
ASSERT_TRUE(
content::NavigateIframeToURL(web_contents, iframe_id, redirect_url));
EndRedirectChain();
// b.test had a stateful redirect, but because it was in an iframe, we ignored
// it.
EXPECT_THAT(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(DIPSBounceDetectorBrowserTest,
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(content::NavigateToURL(web_contents, url));
UserActivationObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
content::WaitForHitTestData(web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
// Verify interaction was recorded for d.test, before proceeding.
absl::optional<StateValue> state = GetDIPSState(url);
ASSERT_TRUE(state.has_value());
ASSERT_TRUE(state->user_interaction_times.has_value());
// Visit initial page on a.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which statefully
// S-redirects to c.test.
ASSERT_TRUE(content::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")));
// Write a cookie via JS on c.test.
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
FrameCookieAccessObserver c_cookie_observer(web_contents, frame,
CookieOperation::kChange);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
c_cookie_observer.Wait();
// Navigate without a click (i.e. by C-redirecting) to d.test.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
// Write a cookie via JS on d.test.
frame = web_contents->GetPrimaryMainFrame();
FrameCookieAccessObserver d_cookie_observer(web_contents, frame,
CookieOperation::kChange);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
d_cookie_observer.Wait();
// 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(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL(
"e.test",
"/cross-site-with-cookie/f.test/cross-site-with-cookie/g.test/"
"title1.html"),
embedded_test_server()->GetURL("g.test", "/title1.html")));
EndRedirectChain();
BlockUntilHelperProcessesPendingRequests();
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(
DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Client_IgnoreThirdPartySubresource) {
// We host the image on an HTTPS server, because for it to read a third-party
// cookie, it needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(kChromeTestDataDir);
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(content::NavigateToURL(
web_contents, https_server.GetURL(
"d.test", "/set-cookie?foo=bar;Secure;SameSite=None")));
// Visit initial page
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, bounce_url));
// Cause a third-party cookie read.
CreateImageAndWaitForCookieAccess(image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
final_url));
EXPECT_THAT(logger->log(),
ElementsAre(
// Set cookie on d.test
("DidStartNavigation(d.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: d.test/set-cookie)"),
("DidFinishNavigation(d.test/set-cookie)"),
// Visit a.test
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
// Bounce on b.test (reading third-party d.test cookie)
("DidStartNavigation(b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Read: d.test/favicon/icon.png)"),
// Land on c.test
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)")));
EndRedirectChain();
// b.test is 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(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_Client_FirstPartySubresource) {
GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
GURL image_url =
embedded_test_server()->GetURL("sub.b.test", "/favicon/icon.png");
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Start logging WebContentsObserver callbacks.
WCOCallbackLogger::CreateForWebContents(web_contents);
auto* logger = WCOCallbackLogger::FromWebContents(web_contents);
// Set cookie on sub.b.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("sub.b.test", "/set-cookie?foo=bar")));
// Visit initial page
ASSERT_TRUE(content::NavigateToURL(web_contents, initial_url));
// Navigate with a click (not a redirect).
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, bounce_url));
// Cause a same-site cookie read.
CreateImageAndWaitForCookieAccess(image_url);
// Navigate without a click (i.e. by redirecting).
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
final_url));
EXPECT_THAT(logger->log(),
ElementsAre(
// Set cookie on sub.b.test
("DidStartNavigation(sub.b.test/set-cookie)"),
("OnCookiesAccessed(NavigationHandle, "
"Change: sub.b.test/set-cookie)"),
("DidFinishNavigation(sub.b.test/set-cookie)"),
// Visit a.test
("DidStartNavigation(a.test/title1.html)"),
("DidFinishNavigation(a.test/title1.html)"),
// Bounce on b.test (reading same-site sub.b.test cookie)
("DidStartNavigation(b.test/title1.html)"),
("DidFinishNavigation(b.test/title1.html)"),
("OnCookiesAccessed(RenderFrameHost, "
"Read: sub.b.test/favicon/icon.png)"),
// Land on c.test
("DidStartNavigation(c.test/title1.html)"),
("DidFinishNavigation(c.test/title1.html)")));
EndRedirectChain();
// b.test IS considered a stateful bounce, even though the cookie was read by
// an image hosted on sub.b.test.
EXPECT_THAT(redirects,
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(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_ServerClientClientServer) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Visit initial page on a.test
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
// Navigate without a click (i.e. by C-redirecting) to d.test
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// S-redirects to f.test
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL("e.test",
"/cross-site/f.test/title1.html"),
embedded_test_server()->GetURL("f.test", "/title1.html")));
EndRedirectChain();
EXPECT_THAT(
redirects,
ElementsAre(("[1/4] a.test/title1.html -> "
"b.test/cross-site/c.test/title1.html (None) -> "
"f.test/title1.html"),
("[2/4] a.test/title1.html -> c.test/title1.html (None) -> "
"f.test/title1.html"),
("[3/4] a.test/title1.html -> d.test/title1.html (None) -> "
"f.test/title1.html"),
("[4/4] a.test/title1.html -> "
"e.test/cross-site/f.test/title1.html (None) -> "
"f.test/title1.html")));
}
IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest,
DetectStatefulRedirect_ClosingTabEndsChain) {
WebContents* web_contents = GetActiveWebContents();
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
// Visit initial page on a.test
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL("b.test",
"/cross-site/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
EXPECT_THAT(redirects, IsEmpty());
content::WebContentsDestroyedWatcher destruction_watcher(web_contents);
web_contents->Close();
destruction_watcher.Wait();
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(DIPSBounceDetectorBrowserTest,
OpenServerRedirectURLInNewTab) {
WebContents* original_tab = chrome_test_utils::GetActiveWebContents(this);
GURL original_tab_url(
embedded_test_server()->GetURL("a.test", "/title1.html"));
ASSERT_TRUE(content::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"));
auto maybe_new_tab = OpenInNewTab(original_tab, new_tab_url);
ASSERT_TRUE(maybe_new_tab.has_value()) << maybe_new_tab.error();
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(*maybe_new_tab, original_tab);
ASSERT_EQ((*maybe_new_tab)->GetLastCommittedURL(),
embedded_test_server()->GetURL("c.test", "/title1.html"));
std::vector<std::string> redirects;
DIPSWebContentsObserver* tab_web_contents_observer =
DIPSWebContentsObserver::FromWebContents(*maybe_new_tab);
tab_web_contents_observer->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
EndRedirectChain();
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(DIPSBounceDetectorBrowserTest,
OpenClientRedirectURLInNewTab) {
WebContents* original_tab = chrome_test_utils::GetActiveWebContents(this);
GURL original_tab_url(
embedded_test_server()->GetURL("a.test", "/title1.html"));
ASSERT_TRUE(content::NavigateToURL(original_tab, original_tab_url));
// Open link in a new tab.
GURL new_tab_url(embedded_test_server()->GetURL("b.test", "/title1.html"));
auto maybe_new_tab = OpenInNewTab(original_tab, new_tab_url);
ASSERT_TRUE(maybe_new_tab.has_value()) << maybe_new_tab.error();
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(original_tab, *maybe_new_tab);
ASSERT_EQ(new_tab_url, (*maybe_new_tab)->GetLastCommittedURL());
std::vector<std::string> redirects;
DIPSWebContentsObserver* tab_web_contents_observer =
DIPSWebContentsObserver::FromWebContents(*maybe_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(content::NavigateToURLFromRendererWithoutUserGesture(
*maybe_new_tab,
embedded_test_server()->GetURL("c.test", "/title1.html")));
EndRedirectChain();
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(DIPSBounceDetectorBrowserTest,
OpenRedirectURLInNewTab_OpaqueOriginInitiator) {
WebContents* original_tab = chrome_test_utils::GetActiveWebContents(this);
GURL original_tab_url("data:text/html,<html></html>");
ASSERT_TRUE(content::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"));
auto maybe_new_tab = OpenInNewTab(original_tab, new_tab_url);
ASSERT_TRUE(maybe_new_tab.has_value()) << maybe_new_tab.error();
// Verify the tab is different from the original and at the correct URL.
EXPECT_NE(*maybe_new_tab, original_tab);
ASSERT_EQ((*maybe_new_tab)->GetLastCommittedURL(),
embedded_test_server()->GetURL("c.test", "/title1.html"));
std::vector<std::string> redirects;
DIPSWebContentsObserver* tab_web_contents_observer =
DIPSWebContentsObserver::FromWebContents(*maybe_new_tab);
tab_web_contents_observer->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
EndRedirectChain();
EXPECT_THAT(redirects,
ElementsAre((
"[1/1] blank -> "
"b.test/cross-site-with-cookie/c.test/title1.html (Write) -> "
"c.test/title1.html")));
}
class DIPSBounceTrackingDevToolsIssueTest
: public content::TestDevToolsProtocolClient,
public DIPSBounceDetectorBrowserTest {
protected:
void WaitForIssueAndCheckTrackingSites(
const std::vector<std::string>& sites) {
auto is_dips_issue = [](const base::Value::Dict& params) {
return *(params.FindStringByDottedPath("issue.code")) ==
"BounceTrackingIssue";
};
// Wait for notification of a Bounce Tracking Issue.
base::Value::Dict params = WaitForMatchingNotification(
"Audits.issueAdded", base::BindRepeating(is_dips_issue));
ASSERT_EQ(*params.FindStringByDottedPath("issue.code"),
"BounceTrackingIssue");
base::Value::Dict* bounce_tracking_issue_details =
params.FindDictByDottedPath("issue.details.bounceTrackingIssueDetails");
ASSERT_TRUE(bounce_tracking_issue_details);
std::vector<std::string> tracking_sites;
base::Value::List* tracking_sites_list =
bounce_tracking_issue_details->FindList("trackingSites");
if (tracking_sites_list) {
for (const auto& val : *tracking_sites_list) {
tracking_sites.push_back(val.GetString());
}
}
// Verify the reported tracking sites match the expected sites.
EXPECT_THAT(tracking_sites, testing::ElementsAreArray(sites));
// Clear existing notifications so subsequent calls don't fail by checking
// `sites` against old notifications.
ClearNotifications();
}
void TearDownOnMainThread() override {
DetachProtocolClient();
DIPSBounceDetectorBrowserTest::TearDownOnMainThread();
}
};
IN_PROC_BROWSER_TEST_F(DIPSBounceTrackingDevToolsIssueTest,
BounceTrackingDevToolsIssue) {
WebContents* web_contents = GetActiveWebContents();
// Visit initial page on a.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
// Open DevTools and enable Audit domain.
AttachToWebContents(web_contents);
SendCommandSync("Audits.enable");
ClearNotifications();
// Navigate with a click (not a redirect) to b.test, which S-redirects to
// c.test.
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL(
"b.test", "/cross-site-with-cookie/c.test/title1.html"),
embedded_test_server()->GetURL("c.test", "/title1.html")));
WaitForIssueAndCheckTrackingSites({"b.test"});
// Write a cookie via JS on c.test.
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
FrameCookieAccessObserver cookie_observer(web_contents, frame,
CookieOperation::kChange);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
// Navigate without a click (i.e. by C-redirecting) to d.test.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
WaitForIssueAndCheckTrackingSites({"c.test"});
// Navigate without a click (i.e. by C-redirecting) to e.test, which
// S-redirects to f.test, which S-redirects to g.test.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
web_contents,
embedded_test_server()->GetURL(
"e.test",
"/cross-site-with-cookie/f.test/cross-site-with-cookie/g.test/"
"title1.html"),
embedded_test_server()->GetURL("g.test", "/title1.html")));
// Note d.test is not listed as a potentially tracking site since it did not
// write cookies before bouncing the user.
WaitForIssueAndCheckTrackingSites({"e.test", "f.test"});
}
class DIPSSiteDataAccessDetectorTest
: public DIPSBounceDetectorBrowserTest,
public testing::WithParamInterface<StorageType> {
public:
DIPSSiteDataAccessDetectorTest(const DIPSSiteDataAccessDetectorTest&) =
delete;
DIPSSiteDataAccessDetectorTest& operator=(
const DIPSSiteDataAccessDetectorTest&) = delete;
DIPSSiteDataAccessDetectorTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
base::FilePath path;
base::PathService::Get(content::DIR_TEST_DATA, &path);
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server_.ServeFilesFromDirectory(path);
https_server_.AddDefaultHandlers(kChromeTestDataDir);
ASSERT_TRUE(https_server_.Start());
SetUpDIPSWebContentsObserver();
}
auto* TestServer() { return &https_server_; }
private:
net::test_server::EmbeddedTestServer https_server_;
};
IN_PROC_BROWSER_TEST_P(DIPSSiteDataAccessDetectorTest,
DetectSiteDataAccess_Storages) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
EXPECT_TRUE(content::NavigateToURLFromRenderer(
GetActiveWebContents()->GetPrimaryMainFrame(),
TestServer()->GetURL("a.test", "/title1.html")));
EXPECT_TRUE(
AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(), GetParam()));
EXPECT_THAT(
logger->log(),
testing::ContainerEq(std::vector<std::string>({
"DidStartNavigation(a.test/title1.html)",
"DidFinishNavigation(a.test/title1.html)",
"OnSiteDataAccessed(AccessDetails, Storage: Unknown: a.test/)",
})));
}
IN_PROC_BROWSER_TEST_P(DIPSSiteDataAccessDetectorTest,
AttributeSameSiteIframesSiteDataAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
TestServer()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = TestServer()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", iframe_url));
EXPECT_TRUE(AccessStorage(GetIFrame(), GetParam()));
const GURL primary_main_frame_final_url =
TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_P(DIPSSiteDataAccessDetectorTest,
AttributeSameSiteNestedIframesSiteDataAccessTo1P) {
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
TestServer()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL iframe_url = TestServer()->GetURL("a.test", "/iframe_blank.html");
ASSERT_TRUE(
content::NavigateIframeToURL(GetActiveWebContents(), "test", iframe_url));
const GURL nested_iframe_url = TestServer()->GetURL("a.test", "/title1.html");
NavigateNestedIFrameTo(GetIFrame(), "test", nested_iframe_url);
EXPECT_TRUE(AccessStorage(GetNestedIFrame(), GetParam()));
const GURL primary_main_frame_final_url =
TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), primary_main_frame_final_url));
CloseTab(GetActiveWebContents());
EXPECT_THAT(redirects, ElementsAre(("[1/1] blank -> a.test/iframe_blank.html "
"(Write) -> d.test/title1.html")));
}
IN_PROC_BROWSER_TEST_P(DIPSSiteDataAccessDetectorTest,
DiscardFencedFrameCookieClientAccess) {
// `StorageType::DATABASE` is disallowed in fenced frames.
if (GetParam() == StorageType::DATABASE) {
GTEST_SKIP();
}
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
TestServer()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL fenced_frame_url =
TestServer()->GetURL("a.test", "/fenced_frames/title2.html");
std::unique_ptr<content::RenderFrameHostWrapper> fenced_frame =
std::make_unique<content::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 =
TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(content::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(DIPSSiteDataAccessDetectorTest,
DiscardPrerenderedPageCookieClientAccess) {
// Prerendering pages do not have access to `StorageType::FILE_SYSTEM` until
// activation (AKA becoming the primary page, whose test case is already
// covered).
if (GetParam() == StorageType::FILE_SYSTEM) {
GTEST_SKIP();
}
std::vector<std::string> redirects;
StartAppendingRedirectsTo(&redirects);
const GURL primary_main_frame_url =
TestServer()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(
content::NavigateToURL(GetActiveWebContents(), primary_main_frame_url));
const GURL prerendering_url = TestServer()->GetURL("a.test", "/title2.html");
const int host_id = prerender_test_helper()->AddPrerender(prerendering_url);
prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
content::test::PrerenderHostObserver observer(*GetActiveWebContents(),
host_id);
EXPECT_FALSE(observer.was_activated());
content::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 =
TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `primary_main_frame_final_url`.
ASSERT_TRUE(content::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
// there are not persistent.
// TODO(crbug.com/1449328): Remove `StorageType::DATABASE` once deprecation is
// complete.
// TODO(crbug.com/1449328): Remove `StorageType::FILE_SYSTEM` once deprecation
// is complete.
INSTANTIATE_TEST_SUITE_P(All,
DIPSSiteDataAccessDetectorTest,
::testing::Values(StorageType::DATABASE,
StorageType::LOCAL_STORAGE,
StorageType::SESSION_STORAGE,
StorageType::CACHE,
StorageType::FILE_SYSTEM,
StorageType::INDEXED_DB));
// WebAuthn tests do not work on Android because there is no current way to
// install a virtual authenticator.
#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 DIPSWebAuthnBrowserTest : public CertVerifierBrowserTest {
public:
DIPSWebAuthnBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
DIPSWebAuthnBrowserTest(const DIPSWebAuthnBrowserTest&) = delete;
DIPSWebAuthnBrowserTest& operator=(const DIPSWebAuthnBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
CertVerifierBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUpOnMainThread() override {
CertVerifierBrowserTest::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(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
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<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
}
void PostRunTestOnMainThread() override {
auth_env_.reset();
CertVerifierBrowserTest::PostRunTestOnMainThread();
}
auto* TestServer() { return &https_server_; }
content::WebContents* GetActiveWebContents() {
return chrome_test_utils::GetActiveWebContents(this);
}
protected:
const std::string authn_hostname = "b.test";
private:
net::EmbeddedTestServer https_server_;
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
IN_PROC_BROWSER_TEST_F(DIPSWebAuthnBrowserTest,
WebAuthnAssertion_ConfirmWCOCallback) {
// Start logging `WebContentsObserver` callbacks.
WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());
auto* web_contents_observer =
DIPSWebContentsObserver::FromWebContents(GetActiveWebContents());
std::vector<std::string> redirects;
web_contents_observer->SetRedirectChainHandlerForTesting(
base::BindRepeating(&AppendRedirects, &redirects));
const GURL initial_url = TestServer()->GetURL("a.test", "/title1.html");
ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), initial_url));
const GURL bounce_url = TestServer()->GetURL(authn_hostname, "/title1.html");
ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), bounce_url));
AccessCookieViaJSIn(GetActiveWebContents(),
GetActiveWebContents()->GetPrimaryMainFrame());
EXPECT_EQ("OK", content::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());
)",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
const GURL final_url = TestServer()->GetURL("d.test", "/title1.html");
// Performs a Client-redirect to `final_url`.
ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture(
GetActiveWebContents(), final_url));
EXPECT_THAT(
logger->log(),
testing::ElementsAre(
"DidStartNavigation(a.test/title1.html)",
"DidFinishNavigation(a.test/title1.html)",
"DidStartNavigation(b.test/title1.html)",
"DidFinishNavigation(b.test/title1.html)",
"OnCookiesAccessed(RenderFrameHost, Change: b.test/title1.html)",
"WebAuthnAssertionRequestSucceeded(b.test/title1.html)",
"DidStartNavigation(d.test/title1.html)",
"DidFinishNavigation(d.test/title1.html)"));
CloseTab(GetActiveWebContents());
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()));
}
#endif // !BUILDFLAG(IS_ANDROID)