| // 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 <iterator> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #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/ranges/algorithm.h" |
| #include "base/run_loop.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.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/time.h" |
| #include "base/types/expected.h" |
| #include "chrome/browser/content_settings/cookie_settings_factory.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/dips/dips_service.h" |
| #include "chrome/browser/dips/dips_service_factory.h" |
| #include "chrome/browser/dips/dips_storage.h" |
| #include "chrome/browser/dips/dips_test_utils.h" |
| #include "chrome/browser/dips/dips_utils.h" |
| #include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/subresource_filter/subresource_filter_browser_test_harness.h" |
| #include "chrome/browser/tpcd/heuristics/opener_heuristic_tab_helper.h" |
| #include "chrome/browser/tpcd/heuristics/redirect_heuristic_tab_helper.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/test/base/chrome_test_utils.h" |
| #include "components/content_settings/core/browser/cookie_settings.h" |
| #include "components/content_settings/core/common/features.h" |
| #include "components/content_settings/core/common/pref_names.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations.h" |
| #include "components/privacy_sandbox/tracking_protection_prefs.h" |
| #include "components/subresource_filter/core/common/test_ruleset_utils.h" |
| #include "content/public/browser/attribution_data_model.h" |
| #include "content/public/browser/browser_context.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/storage_partition.h" |
| #include "content/public/browser/web_contents.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/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/fenced_frame_test_util.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/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 "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/network/public/cpp/features.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/switches.h" |
| #include "third_party/metrics_proto/ukm/source.pb.h" |
| #include "url/gurl.h" |
| #include "url/origin.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/browser/ui/browser.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; |
| using AttributionData = std::set<content::AttributionDataModel::DataKey>; |
| |
| 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<std::string_view>(sites.begin(), sites.end()), ", ")); |
| } |
| |
| std::vector<url::Origin> GetOrigins(const AttributionData& data) { |
| std::vector<url::Origin> origins; |
| base::ranges::transform( |
| data, std::back_inserter(origins), |
| &content::AttributionDataModel::DataKey::reporting_origin); |
| return origins; |
| } |
| |
| } // 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 content::SharedWorkerService::Observer, |
| public content::DedicatedWorkerService::Observer { |
| 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 OnServiceWorkerAccessed( |
| content::RenderFrameHost* render_frame_host, |
| const GURL& scope, |
| content::AllowServiceWorkerResult allowed) override; |
| void OnServiceWorkerAccessed( |
| content::NavigationHandle* navigation_handle, |
| const GURL& scope, |
| content::AllowServiceWorkerResult allowed) 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. |
| |
| // Start SharedWorkerService.Observer overrides: |
| void OnClientAdded( |
| const blink::SharedWorkerToken& token, |
| content::GlobalRenderFrameHostId render_frame_host_id) override; |
| void OnWorkerCreated(const blink::SharedWorkerToken& token, |
| int worker_process_id, |
| const base::UnguessableToken& dev_tools_token) override { |
| } |
| void OnBeforeWorkerDestroyed(const blink::SharedWorkerToken& token) override { |
| } |
| void OnClientRemoved( |
| const blink::SharedWorkerToken& token, |
| content::GlobalRenderFrameHostId render_frame_host_id) override {} |
| using content::SharedWorkerService::Observer::OnFinalResponseURLDetermined; |
| // End SharedWorkerService.Observer overrides. |
| |
| // Start DedicatedWorkerService.Observer overrides: |
| void OnWorkerCreated(const blink::DedicatedWorkerToken& worker_token, |
| int worker_process_id, |
| content::DedicatedWorkerCreator creator) override; |
| void OnBeforeWorkerDestroyed( |
| const blink::DedicatedWorkerToken& worker_token, |
| content::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(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::OnServiceWorkerAccessed( |
| content::RenderFrameHost* render_frame_host, |
| const GURL& scope, |
| content::AllowServiceWorkerResult allowed) { |
| log_.push_back( |
| base::StringPrintf("OnServiceWorkerAccessed(RenderFrameHost: %s)", |
| FormatURL(scope).c_str())); |
| } |
| |
| void WCOCallbackLogger::OnServiceWorkerAccessed( |
| content::NavigationHandle* navigation_handle, |
| const GURL& scope, |
| content::AllowServiceWorkerResult allowed) { |
| log_.push_back( |
| base::StringPrintf("OnServiceWorkerAccessed(NavigationHandle: %s)", |
| FormatURL(scope).c_str())); |
| } |
| |
| void WCOCallbackLogger::OnClientAdded( |
| const blink::SharedWorkerToken& token, |
| content::GlobalRenderFrameHostId render_frame_host_id) { |
| content::RenderFrameHost* render_frame_host = |
| content::RenderFrameHost::FromID(render_frame_host_id); |
| GURL scope = GetFirstPartyURL(render_frame_host).value_or(GURL()); |
| |
| log_.push_back(base::StringPrintf("OnSharedWorkerClientAdded(%s)", |
| FormatURL(scope).c_str())); |
| } |
| |
| void WCOCallbackLogger::OnWorkerCreated( |
| const blink::DedicatedWorkerToken& worker_token, |
| int worker_process_id, |
| content::DedicatedWorkerCreator creator) { |
| const content::GlobalRenderFrameHostId& render_frame_host_id = |
| absl::get<content::GlobalRenderFrameHostId>(creator); |
| content::RenderFrameHost* render_frame_host = |
| content::RenderFrameHost::FromID(render_frame_host_id); |
| GURL scope = GetFirstPartyURL(render_frame_host).value_or(GURL()); |
| |
| log_.push_back(base::StringPrintf("OnDedicatedWorkerCreated(%s)", |
| FormatURL(scope).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 subresource_filter::SubresourceFilterBrowserTest { |
| protected: |
| DIPSBounceDetectorBrowserTest() |
| : prerender_test_helper_(base::BindRepeating( |
| &DIPSBounceDetectorBrowserTest::GetActiveWebContents, |
| base::Unretained(this))) { |
| // WebSQL is disabled by default as of M119 (crbug/695592). Enable feature |
| // in tests during deprecation trial and enterprise policy support. |
| enabled_features_.push_back({blink::features::kWebSQLAccess, {}}); |
| |
| enabled_features_.push_back( |
| {network::features::kSkipTpcdMitigationsForAds, |
| {{"SkipTpcdMitigationsForAdsHeuristics", "true"}}}); |
| } |
| |
| void SetUp() override { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| enabled_features_, |
| /*disabled_features=*/{ |
| // TODO(crbug.com/1394910): Use HTTPS URLs in tests to avoid having |
| // to disable this feature. |
| features::kHttpsUpgrades, |
| }); |
| PlatformBrowserTest::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 { |
| prerender_test_helper_.RegisterServerRequestMonitor(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| SetUpDIPSWebContentsObserver(); |
| |
| // These rules apply an ad-tagging param to cookies marked with the `isad=1` |
| // param value. |
| SetRulesetWithRules( |
| {subresource_filter::testing::CreateSuffixRule("isad=1")}); |
| } |
| |
| void SetUpDIPSWebContentsObserver() { |
| web_contents_observer_ = |
| DIPSWebContentsObserver::FromWebContents(GetActiveWebContents()); |
| CHECK(web_contents_observer_); |
| } |
| |
| content::WebContents* GetActiveWebContents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| 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 DIPSService 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(); |
| DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()); |
| GURL expected_url = web_contents->GetLastCommittedURL(); |
| |
| RedirectChainObserver chain_observer(dips_service, expected_url); |
| // Performing a browser-based navigation terminates the current redirect |
| // chain. |
| ASSERT_TRUE(content::NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("endthechain.test", "/title1.html"))); |
| if (wait) { |
| chain_observer.Wait(); |
| } |
| } |
| |
| [[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); |
| } |
| |
| RedirectChainDetector* GetRedirectChainHelper() { |
| return RedirectChainDetector::FromWebContents(GetActiveWebContents()); |
| } |
| |
| 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(); |
| } |
| |
| void SimulateMouseClick() { |
| SimulateMouseClickAndWait(GetActiveWebContents()); |
| } |
| |
| void SimulateCookieWrite() { |
| WebContents* web_contents = GetActiveWebContents(); |
| content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame(); |
| URLCookieAccessObserver cookie_observer( |
| web_contents, frame->GetLastCommittedURL(), CookieOperation::kChange); |
| ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';", |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| cookie_observer.Wait(); |
| } |
| |
| const base::FilePath kChromeTestDataDir = |
| base::FilePath(FILE_PATH_LITERAL("chrome/test/data")); |
| |
| std::vector<base::test::FeatureRefAndParams> enabled_features_; |
| raw_ptr<DIPSWebContentsObserver, AcrossTasksDanglingUntriaged> |
| web_contents_observer_ = nullptr; |
| |
| private: |
| content::test::PrerenderTestHelper prerender_test_helper_; |
| content::test::FencedFrameTestHelper fenced_frame_test_helper_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| DIPSBounceDetectorBrowserTest, |
| // TODO(crbug.com/1467570): 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", "/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"))); |
| } |
| |
| // TODO(crbug.com/1466483): Flaky on Mac. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \ |
| DISABLED_AttributeSameSiteIframesCookieServerAccessTo1P |
| #else |
| #define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \ |
| AttributeSameSiteIframesCookieServerAccessTo1P |
| #endif |
| IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest, |
| MAYBE_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"); |
| CookieSettingsFactory::GetForProfile( |
| Profile::FromBrowserContext(GetActiveWebContents()->GetBrowserContext())) |
| ->SetThirdPartyCookieSetting( |
| embedded_test_server()->GetURL("a.test", "/"), CONTENT_SETTING_ALLOW); |
| 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, |
| // TODO(crbug.com/1497430): 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(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, |
| // TODO(crbug.com/1497430): 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", "/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, |
| // TODO(crbug.com/1497430): 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(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(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(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(/*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 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(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(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(/*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(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)); |
| SimulateMouseClick(); |
| |
| // Verify interaction was recorded for d.test, before proceeding. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), 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 and write a cookie on 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"))); |
| 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(content::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 |
| // 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(); |
| WaitOnStorage(GetDipsService(web_contents)); |
| |
| 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(web_contents, 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(/*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(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(web_contents, 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(/*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(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(/*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(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()); |
| |
| 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. |
| // TODO(crbug.com/1493269): Flaky on Chrome OS and Linux. |
| #if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) |
| #define MAYBE_OpenServerRedirectURLInNewTab \ |
| DISABLED_OpenServerRedirectURLInNewTab |
| #else |
| #define MAYBE_OpenServerRedirectURLInNewTab OpenServerRedirectURLInNewTab |
| #endif |
| IN_PROC_BROWSER_TEST_F(DIPSBounceDetectorBrowserTest, |
| MAYBE_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")); |
| 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")); |
| |
| std::vector<std::string> redirects; |
| RedirectChainDetector* tab_web_contents_observer = |
| RedirectChainDetector::FromWebContents(new_tab); |
| tab_web_contents_observer->SetRedirectChainHandlerForTesting( |
| base::BindRepeating(&AppendRedirects, &redirects)); |
| |
| EndRedirectChain(/*wait=*/false); |
| |
| 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")); |
| 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(content::NavigateToURLFromRendererWithoutUserGesture( |
| new_tab, embedded_test_server()->GetURL("c.test", "/title1.html"))); |
| EndRedirectChain(/*wait=*/false); |
| |
| 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")); |
| 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")); |
| |
| std::vector<std::string> redirects; |
| RedirectChainDetector* tab_web_contents_observer = |
| RedirectChainDetector::FromWebContents(new_tab); |
| tab_web_contents_observer->SetRedirectChainHandlerForTesting( |
| base::BindRepeating(&AppendRedirects, &redirects)); |
| |
| EndRedirectChain(/*wait=*/false); |
| |
| 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 PlatformBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| PlatformBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| WebContents* GetActiveWebContents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| // Perform a browser-based navigation to terminate the current redirect chain. |
| void EndRedirectChain() { |
| ASSERT_TRUE(content::NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL("endthechain.test", "/title1.html"))); |
| } |
| |
| void SimulateMouseClick() { |
| SimulateMouseClickAndWait(GetActiveWebContents()); |
| } |
| }; |
| |
| // Tests the conditions for recording RedirectHeuristic_CookieAccess2 and |
| // RedirectHeuristic_CookieAccessThirdParty2 UKM events. |
| IN_PROC_BROWSER_TEST_F(RedirectHeuristicBrowserTest, |
| 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( |
| base::FilePath(FILE_PATH_LITERAL("chrome/test/data"))); |
| 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"); |
| |
| // Initialize 3PC settings for the target site. |
| HostContentSettingsMap* map = HostContentSettingsMapFactory::GetForProfile( |
| web_contents->GetBrowserContext()); |
| map->SetContentSettingCustomScope( |
| ContentSettingsPattern::Wildcard(), |
| ContentSettingsPattern::FromString("[*.]" + target_url.host()), |
| ContentSettingsType::COOKIES, ContentSetting::CONTENT_SETTING_ALLOW); |
| |
| // 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(content::NavigateToURL(web_contents, initial_url)); |
| // Redirect to tracking URL. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, tracker_url_pre_target_redirect)); |
| |
| // Redirect to target URL. |
| ASSERT_TRUE(content::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(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, tracker_url_post_target_redirect)); |
| // Redirect to final URL. |
| ASSERT_TRUE(content::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(https://crbug.com/1489241): 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( |
| base::FilePath(FILE_PATH_LITERAL("chrome/test/data"))); |
| ASSERT_TRUE(https_server.Start()); |
| |
| GURL final_url = embedded_test_server()->GetURL("a.test", "/title1.html"); |
| |
| GURL tracker_url_with_interaction = |
| embedded_test_server()->GetURL("b.test", "/title1.html"); |
| GURL image_url_with_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 target_url_3pc_allowed = |
| embedded_test_server()->GetURL("d.test", "/title1.html"); |
| GURL target_url_3pc_blocked = |
| embedded_test_server()->GetURL("e.test", "/iframe_blank.html"); |
| |
| // Initialize 3PC settings for the target sites. |
| HostContentSettingsMap* map = HostContentSettingsMapFactory::GetForProfile( |
| web_contents->GetBrowserContext()); |
| map->SetContentSettingCustomScope(ContentSettingsPattern::Wildcard(), |
| ContentSettingsPattern::FromString( |
| "[*.]" + target_url_3pc_allowed.host()), |
| ContentSettingsType::COOKIES, |
| ContentSetting::CONTENT_SETTING_ALLOW); |
| map->SetContentSettingCustomScope(ContentSettingsPattern::Wildcard(), |
| ContentSettingsPattern::FromString( |
| "[*.]" + target_url_3pc_blocked.host()), |
| ContentSettingsType::COOKIES, |
| ContentSetting::CONTENT_SETTING_BLOCK); |
| |
| // 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)); |
| |
| // Start on `tracker_url_with_interaction` and record a current interaction. |
| ASSERT_TRUE( |
| content::NavigateToURL(web_contents, tracker_url_with_interaction)); |
| SimulateMouseClick(); |
| |
| // Redirect to one of the target URLs, to set DoesFirstPartyPrecedeThirdParty. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, target_url_3pc_blocked)); |
| // Redirect to all tracking URLs. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, tracker_url_in_iframe)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, tracker_url_with_interaction)); |
| |
| // Redirect to target URL with cookies allowed. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, target_url_3pc_allowed)); |
| // Read a cookie from the tracking URL with interaction. |
| CreateImageAndWaitForCookieAccess( |
| web_contents, |
| https_server.GetURL("sub.b.test", "/favicon/icon.png?isad=1")); |
| |
| // Redirect to target URL with cookies blocked. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, target_url_3pc_blocked)); |
| // Open an iframe of the tracking URL on the target URL. |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents, |
| /*iframe_id=*/"test", |
| 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(content::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"}); |
| |
| // Expect UKM entries from both of the cookie accesses. |
| ASSERT_EQ(2u, 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 interaction |
| // within the last hour, on a site with 3PC access allowed. |
| |
| // 1 site was passed: tracker_url_with_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"), 1); |
| EXPECT_EQ(ukm_entries[0].metrics.at("DoesFirstPartyPrecedeThirdParty"), |
| false); |
| EXPECT_EQ(ukm_entries[0].metrics.at("IsCurrentInteraction"), 1); |
| |
| // The third cookie access was from a tracking site in an iframe of the |
| // target, on a site with 3PC access blocked. |
| |
| // 3 sites were passed: tracker_url_in_iframe -> tracker_url_with_interaction |
| // -> target_url_3pc_allowed -> target_url_3pc_blocked |
| 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_blocked)); |
| EXPECT_EQ(ukm_entries[1].metrics.at("AccessAllowed"), false); |
| EXPECT_EQ(ukm_entries[1].metrics.at("IsAdTagged"), |
| static_cast<int32_t>(OptionalBool::kFalse)); |
| EXPECT_EQ(ukm_entries[1].metrics.at("HoursSinceLastInteraction"), -1); |
| EXPECT_EQ(ukm_entries[1].metrics.at("OpenerHasSameSiteIframe"), |
| static_cast<int32_t>(OptionalBool::kTrue)); |
| EXPECT_EQ(ukm_entries[1].metrics.at("SitesPassedCount"), 3); |
| EXPECT_EQ(ukm_entries[1].metrics.at("DoesFirstPartyPrecedeThirdParty"), true); |
| EXPECT_EQ(ukm_entries[1].metrics.at("IsCurrentInteraction"), 0); |
| |
| // Verify there are 2 corresponding CookieAccessThirdParty entries with |
| // matching access IDs. |
| std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> |
| ukm_third_party_entries = ukm_recorder.GetEntries( |
| "RedirectHeuristic.CookieAccessThirdParty2", {"AccessId"}); |
| ASSERT_EQ(2u, ukm_third_party_entries.size()); |
| |
| EXPECT_THAT( |
| ukm_recorder.GetSourceForSourceId(ukm_third_party_entries[0].source_id) |
| ->url(), |
| Eq(tracker_url_with_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_in_iframe)); |
| EXPECT_EQ(ukm_third_party_entries[1].metrics.at("AccessId"), access_id_2); |
| } |
| |
| struct RedirectHeuristicFlags { |
| bool write_redirect_grants = false; |
| bool require_aba_flow = true; |
| bool require_current_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 = |
| GetParam().require_aba_flow ? "true" : "false"; |
| std::string require_current_interaction_string = |
| GetParam().require_current_interaction ? "true" : "false"; |
| |
| 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=*/{ |
| // TODO(crbug.com/1394910): Use HTTPS URLs in tests to avoid having |
| // to disable this feature. |
| features::kHttpsUpgrades, |
| }); |
| 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()->profile()->GetPrefs()->SetInteger( |
| prefs::kCookieControlsMode, |
| static_cast<int>( |
| content_settings::CookieControlsMode::kBlockThirdParty)); |
| browser()->profile()->GetPrefs()->SetBoolean( |
| prefs::kTrackingProtection3pcdEnabled, true); |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| std::vector<base::test::FeatureRefAndParams> enabled_features_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(RedirectHeuristicGrantTest, |
| CreatesRedirectHeuristicGrantsWithSatisfyingURL) { |
| WebContents* web_contents = GetActiveWebContents(); |
| auto cookie_settings = CookieSettingsFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext())); |
| |
| // 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(content::NavigateToURL(web_contents, first_party_url)); |
| |
| // Navigate to `aba_current_interaction_url` and record a current interaction. |
| ASSERT_TRUE( |
| content::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(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, first_party_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, aba_current_interaction_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, no_interaction_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, first_party_url)); |
| EndRedirectChain(); |
| |
| // Wait on async tasks for the grants to be created. |
| WaitOnStorage(GetDipsService(web_contents)); |
| |
| // Expect some cookie grants on `first_party_url` based on flags and criteria. |
| EXPECT_EQ(cookie_settings->GetCookieSetting( |
| aba_current_interaction_url, first_party_url, |
| net::CookieSettingOverrides(), nullptr), |
| GetParam().write_redirect_grants ? CONTENT_SETTING_ALLOW |
| : CONTENT_SETTING_BLOCK); |
| EXPECT_EQ( |
| cookie_settings->GetCookieSetting(no_interaction_url, first_party_url, |
| net::CookieSettingOverrides(), nullptr), |
| CONTENT_SETTING_BLOCK); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| RedirectHeuristicGrantTest, |
| CreatesRedirectHeuristicGrantsWithPartiallySatisfyingURL) { |
| WebContents* web_contents = GetActiveWebContents(); |
| auto cookie_settings = CookieSettingsFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext())); |
| |
| // 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(content::NavigateToURL(web_contents, aba_past_interaction_url)); |
| SimulateMouseClick(); |
| |
| // Start redirect chain on `no_aba_current_interaction_url` and record a |
| // current interaction. |
| ASSERT_TRUE( |
| content::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(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, no_aba_current_interaction_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, first_party_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, aba_past_interaction_url)); |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, first_party_url)); |
| EndRedirectChain(); |
| |
| // Wait on async tasks for the grants to be created. |
| WaitOnStorage(GetDipsService(web_contents)); |
| |
| // Expect some cookie grants on `first_party_url` based on flags and criteria. |
| EXPECT_EQ(cookie_settings->GetCookieSetting( |
| aba_past_interaction_url, first_party_url, |
| net::CookieSettingOverrides(), nullptr), |
| (GetParam().write_redirect_grants && |
| !GetParam().require_current_interaction) |
| ? CONTENT_SETTING_ALLOW |
| : CONTENT_SETTING_BLOCK); |
| EXPECT_EQ(cookie_settings->GetCookieSetting( |
| no_aba_current_interaction_url, first_party_url, |
| net::CookieSettingOverrides(), nullptr), |
| (GetParam().write_redirect_grants && !GetParam().require_aba_flow) |
| ? CONTENT_SETTING_ALLOW |
| : CONTENT_SETTING_BLOCK); |
| } |
| |
| 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, |
| }, |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| RedirectHeuristicGrantTest, |
| ::testing::ValuesIn(kRedirectHeuristicTestCases)); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| 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 |
| // they 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. |
| // NOTE: Manual testing was performed to ensure this implementation works as |
| // expected on Android platform. |
| // TODO(crbug.com/1449328): 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 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); |
| |
| CertVerifierBrowserTest::host_resolver()->AddRule("*", "127.0.0.1"); |
| https_server_.ServeFilesFromSourceDirectory( |
| base::FilePath(FILE_PATH_LITERAL("chrome/test/data"))); |
| 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<content::ScopedAuthenticatorEnvironmentForTesting>( |
| std::move(virtual_device_factory)); |
| |
| web_contents_observer_ = |
| DIPSWebContentsObserver::FromWebContents(GetActiveWebContents()); |
| CHECK(web_contents_observer_); |
| } |
| |
| void TearDownOnMainThread() override { |
| CertVerifierBrowserTest::TearDownOnMainThread(); |
| web_contents_observer_ = nullptr; |
| } |
| |
| void PostRunTestOnMainThread() override { |
| auth_env_.reset(); |
| // web_contents_observer_.ClearAndDelete(); |
| CertVerifierBrowserTest::PostRunTestOnMainThread(); |
| } |
| |
| auto* TestServer() { return &https_server_; } |
| |
| content::WebContents* GetActiveWebContents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| 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( |
| content::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", 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)); |
| } |
| |
| protected: |
| const std::string authn_hostname = "b.test"; |
| |
| private: |
| net::EmbeddedTestServer https_server_; |
| raw_ptr<DIPSWebContentsObserver> web_contents_observer_ = nullptr; |
| 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()); |
| |
| std::vector<std::string> redirects; |
| StartAppendingRedirectsTo(&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()); |
| |
| GetWebAuthnAssertion(); |
| |
| 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)")); |
| |
| 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( |
| DIPSWebAuthnBrowserTest, |
| ReportRedirectorsInChain_OmitSitesWithWebAuthnAssertions) { |
| WebContents* web_contents = GetActiveWebContents(); |
| |
| std::vector<std::string> reports; |
| StartAppendingReportsTo(&reports); |
| |
| // Visit initial page on a.test. |
| ASSERT_TRUE(content::NavigateToURL( |
| web_contents, TestServer()->GetURL("a.test", "/title1.html"))); |
| |
| GURL url = TestServer()->GetURL(authn_hostname, "/title1.html"); |
| ASSERT_TRUE( |
| content::NavigateToURLFromRendererWithoutUserGesture(web_contents, url)); |
| |
| GetWebAuthnAssertion(); |
| |
| // Verify web authn assertion was recorded for `authn_hostname`, before |
| // proceeding. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_TRUE(state.has_value()); |
| ASSERT_FALSE(state->user_interaction_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(content::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(content::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(content::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(GetDipsService(web_contents)); |
| |
| EXPECT_THAT(reports, ElementsAre(("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(DIPSBounceDetectorBrowserTest, |
| 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(content::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, content::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/blank.html"))); |
| ASSERT_TRUE(content::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/154571): 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(DIPSBounceDetectorBrowserTest, |
| 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(content::NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL( |
| "a.test", "/private_network_access/no-favicon.html"))); |
| |
| // Create and start a shared worker on the current page. |
| ASSERT_EQ(true, content::EvalJs(GetActiveWebContents(), |
| content::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/" |
| "private_network_access/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(DIPSBounceDetectorBrowserTest, |
| 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(content::NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL( |
| "a.test", "/private_network_access/no-favicon.html"))); |
| |
| // Create and start a dedicated worker on the current page. |
| ASSERT_EQ(true, content::EvalJs(GetActiveWebContents(), |
| content::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/" |
| "private_network_access/no-favicon.html)")); |
| |
| // Clean up the observer to avoid a dangling ptr. |
| GetActiveWebContents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetDedicatedWorkerService() |
| ->RemoveObserver(logger); |
| } |
| |
| class DIPSThrottlingBrowserTest : public DIPSBounceDetectorBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| DIPSBounceDetectorBrowserTest::SetUpOnMainThread(); |
| DIPSWebContentsObserver::FromWebContents(GetActiveWebContents()) |
| ->SetClockForTesting(&test_clock_); |
| } |
| |
| base::SimpleTestClock test_clock_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DIPSThrottlingBrowserTest, |
| 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(content::NavigateToURL(web_contents, url)); |
| SimulateMouseClick(); |
| // Verify the interaction was recorded in the DIPS DB. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->user_interaction_times, |
| testing::Optional(testing::Pair(start_time, start_time))); |
| |
| // Click again, just before kDIPSTimestampUpdateInterval elapses. |
| test_clock_.Advance(kDIPSTimestampUpdateInterval - base::Seconds(1)); |
| SimulateMouseClick(); |
| // Verify the second interaction was NOT recorded, due to throttling. |
| state = GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->user_interaction_times, |
| testing::Optional(testing::Pair(start_time, start_time))); |
| |
| // Click a third time, after kDIPSTimestampUpdateInterval has passed since the |
| // first click. |
| test_clock_.Advance(base::Seconds(1)); |
| SimulateMouseClick(); |
| // Verify the third interaction WAS recorded. |
| state = GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->user_interaction_times, |
| testing::Optional(testing::Pair( |
| start_time, start_time + kDIPSTimestampUpdateInterval))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DIPSThrottlingBrowserTest, |
| 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(content::NavigateToURL(web_contents, url)); |
| SimulateMouseClick(); |
| // Verify the interaction was recorded in the DIPS DB. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->user_interaction_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(content::NavigateToURL(web_contents, url2)); |
| SimulateMouseClick(); |
| // Verify the second interaction was also recorded (not throttled). |
| state = GetDIPSState(GetDipsService(web_contents), url2); |
| ASSERT_THAT(state->user_interaction_times, |
| testing::Optional(testing::Pair(start_time + base::Seconds(1), |
| start_time + base::Seconds(1)))); |
| } |
| |
| // TODO(b/325196134): Re-enable the test. |
| IN_PROC_BROWSER_TEST_F(DIPSThrottlingBrowserTest, |
| DISABLED_StorageRecording_Throttled) { |
| WebContents* web_contents = GetActiveWebContents(); |
| const base::Time start_time = test_clock_.Now(); |
| |
| // Record client-side storage access on a.test. |
| const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html"); |
| ASSERT_TRUE(content::NavigateToURL(web_contents, url)); |
| SimulateCookieWrite(); |
| // Verify the write was recorded in the DIPS DB. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->site_storage_times, |
| testing::Optional(testing::Pair(start_time, start_time))); |
| |
| // Write a cookie again, just before kDIPSTimestampUpdateInterval elapses. |
| test_clock_.Advance(kDIPSTimestampUpdateInterval - base::Seconds(1)); |
| SimulateCookieWrite(); |
| // Verify the second write was NOT recorded, due to throttling. |
| state = GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->site_storage_times, |
| testing::Optional(testing::Pair(start_time, start_time))); |
| |
| // Write a third time, after kDIPSTimestampUpdateInterval has passed since the |
| // first write. |
| test_clock_.Advance(base::Seconds(1)); |
| SimulateCookieWrite(); |
| // Verify the third write WAS recorded. |
| state = GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->site_storage_times, |
| testing::Optional(testing::Pair( |
| start_time, start_time + kDIPSTimestampUpdateInterval))); |
| } |
| |
| // TODO(b/325196134): Re-enable the test. |
| IN_PROC_BROWSER_TEST_F(DIPSThrottlingBrowserTest, |
| DISABLED_StorageRecording_NotThrottled_AfterRefresh) { |
| WebContents* web_contents = GetActiveWebContents(); |
| const base::Time start_time = test_clock_.Now(); |
| |
| // Record client-side storage access on a.test. |
| const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html"); |
| ASSERT_TRUE(content::NavigateToURL(web_contents, url)); |
| SimulateCookieWrite(); |
| // Verify the write was recorded in the DIPS DB. |
| std::optional<StateValue> state = |
| GetDIPSState(GetDipsService(web_contents), url); |
| ASSERT_THAT(state->site_storage_times, |
| testing::Optional(testing::Pair(start_time, start_time))); |
| |
| // Navigate to a new page and write cookies again, only a second after the |
| // previous write. |
| test_clock_.Advance(base::Seconds(1)); |
| const GURL url2 = embedded_test_server()->GetURL("b.test", "/title1.html"); |
| ASSERT_TRUE(content::NavigateToURL(web_contents, url2)); |
| SimulateCookieWrite(); |
| // Verify the second write was also recorded (not throttled). |
| state = GetDIPSState(GetDipsService(web_contents), url2); |
| ASSERT_THAT(state->site_storage_times, |
| testing::Optional(testing::Pair(start_time + base::Seconds(1), |
| start_time + base::Seconds(1)))); |
| } |
| |
| class AllSitesFollowingFirstPartyTest : public PlatformBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| PlatformBrowserTest::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 chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| protected: |
| GURL first_party_url_; |
| GURL third_party_url_; |
| GURL other_url_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, |
| SiteFollowingFirstPartyIncluded) { |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), first_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| |
| EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty( |
| GetActiveWebContents(), first_party_url_), |
| testing::ElementsAre(GetSiteForDIPS(third_party_url_))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, |
| SiteNotFollowingFirstPartyNotIncluded) { |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), first_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| |
| EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty( |
| GetActiveWebContents(), first_party_url_), |
| testing::ElementsAre(GetSiteForDIPS(third_party_url_))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, MultipleSitesIncluded) { |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), first_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), first_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| |
| EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty( |
| GetActiveWebContents(), first_party_url_), |
| testing::ElementsAre(GetSiteForDIPS(third_party_url_), |
| GetSiteForDIPS(other_url_))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, |
| NoFirstParty_NothingIncluded) { |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| |
| EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty( |
| GetActiveWebContents(), first_party_url_), |
| testing::IsEmpty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AllSitesFollowingFirstPartyTest, |
| NothingAfterFirstParty_NothingIncluded) { |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), other_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), third_party_url_)); |
| ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), first_party_url_)); |
| |
| EXPECT_THAT(RedirectHeuristicTabHelper::AllSitesFollowingFirstParty( |
| GetActiveWebContents(), first_party_url_), |
| testing::IsEmpty()); |
| } |
| |
| class DIPSPrivacySandboxDataTest : public PlatformBrowserTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| DIPSPrivacySandboxDataTest() |
| : 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); |
| (ShouldPreservePSData() ? enabled_features : disabled_features) |
| .emplace_back(features::kDIPSPreservePSData); |
| scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| bool ShouldPreservePSData() const { return GetParam(); } |
| |
| void SetUpOnMainThread() override { |
| // Enable Privacy Sandbox APIs on all sites. |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAllPrivacySandboxAttestedForTesting(true); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_https_test_server_.ServeFilesFromSourceDirectory( |
| "content/test/data/"); |
| embedded_https_test_server_.SetSSLConfig( |
| net::EmbeddedTestServer::CERT_TEST_NAMES); |
| ASSERT_TRUE(embedded_https_test_server_.Start()); |
| } |
| |
| content::WebContents* GetActiveWebContents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| base::expected<AttributionData, std::string> WaitForAttributionData() { |
| WebContents* web_contents = GetActiveWebContents(); |
| content::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 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_; |
| |
| private: |
| static void Sleep(base::TimeDelta delay) { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), delay); |
| run_loop.Run(); |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(DIPSPrivacySandboxDataTest, |
| DontClearAttributionReportingApiData) { |
| WebContents* web_contents = GetActiveWebContents(); |
| // Enable Privacy Sandbox APIs in the current profile. |
| PrivacySandboxSettingsFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext())) |
| ->SetAllPrivacySandboxAllowedForTesting(); |
| |
| GURL toplevel_url = |
| embedded_https_test_server_.GetURL("a.test", "/title1.html"); |
| ASSERT_TRUE(content::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(content::ExecJs(web_contents, content::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 DIPS deletion. |
| DIPSService* dips = DIPSService::Get(web_contents->GetBrowserContext()); |
| ASSERT_TRUE(dips != nullptr); |
| base::test::TestFuture<void> record_bounce; |
| dips->storage() |
| ->AsyncCall(&DIPSStorage::RecordBounce) |
| .WithArgs(attribution_url, base::Time::Now(), /*stateful=*/true) |
| .Then(record_bounce.GetCallback()); |
| ASSERT_TRUE(record_bounce.Wait()); |
| |
| // Trigger DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| dips->DeleteEligibleSitesImmediately(deleted_sites.GetCallback()); |
| EXPECT_THAT(deleted_sites.Get(), |
| ElementsAre(GetSiteForDIPS(attribution_url))); |
| |
| base::test::TestFuture<AttributionData> post_deletion_data; |
| web_contents->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetAttributionDataModel() |
| ->GetAllDataKeys(post_deletion_data.GetCallback()); |
| if (ShouldPreservePSData()) { |
| // Confirm the attribution data was not deleted. |
| EXPECT_THAT(GetOrigins(post_deletion_data.Get()), |
| ElementsAre(url::Origin::Create(attribution_url))); |
| } else { |
| // Confirm the attribution data was deleted. |
| EXPECT_THAT(post_deletion_data.Get(), IsEmpty()); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, DIPSPrivacySandboxDataTest, ::testing::Bool()); |
| |
| namespace { |
| |
| class SiteStorage { |
| public: |
| constexpr SiteStorage() = default; |
| |
| virtual base::expected<std::string, std::string> ReadValue( |
| content::RenderFrameHost* frame) const = 0; |
| virtual testing::AssertionResult WriteValue(content::RenderFrameHost* frame, |
| base::StringPiece value, |
| bool partitioned) const = 0; |
| |
| virtual std::string_view name() const = 0; |
| }; |
| |
| class CookieStorage : public SiteStorage { |
| base::expected<std::string, std::string> ReadValue( |
| content::RenderFrameHost* frame) const override { |
| content::EvalJsResult result = content::EvalJs( |
| frame, "document.cookie", content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| if (!result.error.empty()) { |
| return base::unexpected(result.error); |
| } |
| return base::ok(result.ExtractString()); |
| } |
| |
| testing::AssertionResult WriteValue(content::RenderFrameHost* frame, |
| base::StringPiece 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 = content::ExecJs( |
| frame, content::JsReplace("document.cookie = $1;", value), |
| content::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( |
| content::RenderFrameHost* frame) const override { |
| content::EvalJsResult result = |
| content::EvalJs(frame, "localStorage.getItem('value')", |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| if (!result.error.empty()) { |
| return base::unexpected(result.error); |
| } |
| if (result.value.is_none()) { |
| return base::ok(""); |
| } |
| return base::ok(result.ExtractString()); |
| } |
| |
| testing::AssertionResult WriteValue(content::RenderFrameHost* frame, |
| base::StringPiece value, |
| bool partitioned) const override { |
| return content::ExecJs( |
| frame, content::JsReplace("localStorage.setItem('value', $1);", value), |
| content::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 DIPSDataDeletionBrowserTest |
| : public DIPSBounceDetectorBrowserTest, |
| public testing::WithParamInterface<const SiteStorage*> { |
| public: |
| void SetUpOnMainThread() override { |
| DIPSBounceDetectorBrowserTest::SetUpOnMainThread(); |
| https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| https_server_.AddDefaultHandlers(kChromeTestDataDir); |
| ASSERT_TRUE(https_server_.Start()); |
| |
| chrome_test_utils::GetProfile(this)->GetPrefs()->SetInteger( |
| prefs::kCookieControlsMode, |
| static_cast<int>( |
| content_settings::CookieControlsMode::kBlockThirdParty)); |
| } |
| |
| const net::EmbeddedTestServer& https_server() const { return https_server_; } |
| |
| [[nodiscard]] testing::AssertionResult WriteToPartitionedStorage( |
| base::StringPiece first_party_hostname, |
| base::StringPiece third_party_hostname, |
| base::StringPiece value) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| if (!content::NavigateToURL(web_contents, |
| https_server().GetURL(first_party_hostname, |
| "/iframe_blank.html"))) { |
| return testing::AssertionFailure() << "Failed to navigate top-level"; |
| } |
| |
| const base::StringPiece kIframeId = "test"; |
| if (!content::NavigateIframeToURL( |
| web_contents, kIframeId, |
| https_server().GetURL(third_party_hostname, "/title1.html"))) { |
| return testing::AssertionFailure() << "Failed to navigate iframe"; |
| } |
| |
| content::RenderFrameHost* iframe = content::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(base::StringPiece first_party_hostname, |
| base::StringPiece third_party_hostname) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| if (!content::NavigateToURL(web_contents, |
| https_server().GetURL(first_party_hostname, |
| "/iframe_blank.html"))) { |
| return base::unexpected("Failed to navigate top-level"); |
| } |
| |
| const base::StringPiece kIframeId = "test"; |
| if (!content::NavigateIframeToURL( |
| web_contents, kIframeId, |
| https_server().GetURL(third_party_hostname, "/title1.html"))) { |
| return base::unexpected("Failed to navigate iframe"); |
| } |
| |
| content::RenderFrameHost* iframe = content::ChildFrameAt(web_contents, 0); |
| if (!iframe) { |
| return base::unexpected("iframe not found"); |
| } |
| return ReadValue(iframe); |
| } |
| |
| [[nodiscard]] base::expected<std::string, std::string> ReadFromStorage( |
| base::StringPiece hostname) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| if (!content::NavigateToURL( |
| web_contents, https_server().GetURL(hostname, "/title1.html"))) { |
| return base::unexpected("Failed to navigate"); |
| } |
| |
| return ReadValue(web_contents); |
| } |
| |
| [[nodiscard]] testing::AssertionResult WriteToStorage( |
| base::StringPiece hostname, |
| base::StringPiece value) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| if (!content::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( |
| base::StringPiece host1, |
| base::StringPiece host2, |
| base::StringPiece host3) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| if (!content::NavigateToURL(web_contents, |
| https_server().GetURL(host1, "/title1.html"))) { |
| return testing::AssertionFailure() << "Failed to navigate to " << host1; |
| } |
| |
| if (!content::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 (!content::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 content::ToRenderFrameHost& frame) { |
| return storage()->ReadValue(frame.render_frame_host()); |
| } |
| |
| [[nodiscard]] testing::AssertionResult WriteValue( |
| const content::ToRenderFrameHost& frame, |
| base::StringPiece 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(DIPSDataDeletionBrowserTest, DeleteDomain) { |
| content::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 DIPS won't delete its state while loaded. |
| ASSERT_TRUE(content::NavigateToURL( |
| web_contents, https_server().GetURL("a.test", "/title1.html"))); |
| |
| // Trigger DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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(DIPSDataDeletionBrowserTest, DontDeleteOtherDomains) { |
| content::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 DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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(DIPSDataDeletionBrowserTest, |
| DontDeleteDomainWhenPartitioned) { |
| content::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 DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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(DIPSDataDeletionBrowserTest, DeleteSubdomains) { |
| content::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 DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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(DIPSDataDeletionBrowserTest, DeleteEmbedded3Ps) { |
| if (GetParam() == &kCookieStorage) { |
| GTEST_SKIP(); |
| } |
| |
| content::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 DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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(DIPSDataDeletionBrowserTest, |
| DeleteEmbedded3Ps_Subdomain) { |
| if (GetParam() == &kCookieStorage) { |
| GTEST_SKIP(); |
| } |
| |
| content::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 DIPS deletion. |
| base::test::TestFuture<const std::vector<std::string>&> deleted_sites; |
| DIPSService::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("")); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| DIPSDataDeletionBrowserTest, |
| ::testing::Values(&kCookieStorage, &kLocalStorage)); |