From c246dbfaf280f7261eebe1475fe329af30c19c93 Mon Sep 17 00:00:00 2001 From: vadims <118171981+vadimstruts@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:01:37 +0100 Subject: [PATCH] [Shred] Implement back-end for manual site data shredding (#32197) Implemented an ability to enforce shred by user request for desktop and android, it means site data cleaning and and related tabs closing. Resolves: https://github.com/brave/brave-browser/issues/50670 --------- Signed-off-by: Vadym Struts --- browser/brave_shields/DEPS | 1 + .../brave_shields/brave_shields_tab_helper.cc | 20 +- .../brave_shields/brave_shields_tab_helper.h | 4 + browser/brave_shields/sources.gni | 3 + browser/ephemeral_storage/DEPS | 1 + ...rave_ephemeral_storage_service_delegate.cc | 129 ++++++++- ...brave_ephemeral_storage_service_delegate.h | 13 +- .../ephemeral_storage_browsertest.cc | 252 +++++++++++++++++- .../ephemeral_storage_service_factory.cc | 5 +- .../ephemeral_storage_service_unittest.cc | 55 ++-- .../ephemeral_storage_tab_helper.cc | 6 + .../ephemeral_storage_tab_helper.h | 2 + .../tld_ephemeral_lifetime.cc | 7 +- .../tld_ephemeral_lifetime.h | 2 + browser/ui/brave_browser.cc | 23 ++ browser/ui/brave_browser.h | 10 + .../browser/brave_shields_settings_service.cc | 39 +++ .../browser/brave_shields_settings_service.h | 2 + ...brave_shields_settings_service_unittest.cc | 183 +++++++++++++ components/ephemeral_storage/BUILD.gn | 1 + .../ephemeral_storage_service.cc | 40 ++- .../ephemeral_storage_service.h | 8 +- .../ephemeral_storage_service_delegate.h | 7 + 23 files changed, 765 insertions(+), 48 deletions(-) diff --git a/browser/brave_shields/DEPS b/browser/brave_shields/DEPS index 1be40b5635a..e019c0d5b4f 100644 --- a/browser/brave_shields/DEPS +++ b/browser/brave_shields/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+brave/components/brave_shields/core/browser", + "+brave/components/ephemeral_storage", "+brave/components/brave_shields/content/test", "+services/network/host_resolver.h", ] diff --git a/browser/brave_shields/brave_shields_tab_helper.cc b/browser/brave_shields/brave_shields_tab_helper.cc index 6b1abb9a127..38d8f0fb3ac 100644 --- a/browser/brave_shields/brave_shields_tab_helper.cc +++ b/browser/brave_shields/brave_shields_tab_helper.cc @@ -16,6 +16,7 @@ #include "base/types/cxx23_to_underlying.h" #include "brave/browser/brave_shields/brave_shields_settings_service_factory.h" #include "brave/browser/brave_shields/brave_shields_web_contents_observer.h" +#include "brave/browser/ephemeral_storage/ephemeral_storage_service_factory.h" #include "brave/components/brave_shields/core/browser/brave_shields_locale_utils.h" #include "brave/components/brave_shields/core/browser/brave_shields_settings_service.h" #include "brave/components/brave_shields/core/browser/brave_shields_utils.h" @@ -65,8 +66,9 @@ BraveShieldsTabHelper::BraveShieldsTabHelper(content::WebContents* web_contents) web_contents->GetBrowserContext()))), brave_shields_settings_( CHECK_DEREF(BraveShieldsSettingsServiceFactory::GetForProfile( - Profile::FromBrowserContext( - web_contents->GetBrowserContext())))) { + Profile::FromBrowserContext(web_contents->GetBrowserContext())))), + ephemeral_storage_service_(EphemeralStorageServiceFactory::GetForContext( + web_contents->GetBrowserContext())) { favicon::ContentFaviconDriver::FromWebContents(web_contents) ->AddObserver(this); observation_.Observe(&*host_content_settings_map_); @@ -476,6 +478,20 @@ void BraveShieldsTabHelper::BlockAllowedScripts( ReloadWebContents(); } +void BraveShieldsTabHelper::EnforceSiteDataCleanup() { + auto* site_instance = web_contents()->GetSiteInstance(); + CHECK(site_instance); + + if (!ephemeral_storage_service_) { + return; + } + + // Start manual cleanup. + ephemeral_storage_service_->CleanupTLDFirstPartyStorage( + web_contents()->GetLastCommittedURL(), + site_instance->GetStoragePartitionConfig(), true); +} + void BraveShieldsTabHelper::AllowScriptsOnce( const std::vector& origins) { BraveShieldsWebContentsObserver* observer = diff --git a/browser/brave_shields/brave_shields_tab_helper.h b/browser/brave_shields/brave_shields_tab_helper.h index 61668f2f3ff..4c1c2b04d9b 100644 --- a/browser/brave_shields/brave_shields_tab_helper.h +++ b/browser/brave_shields/brave_shields_tab_helper.h @@ -17,6 +17,7 @@ #include "base/scoped_observation.h" #include "brave/components/brave_shields/core/common/brave_shields_panel.mojom.h" #include "brave/components/brave_shields/core/common/shields_settings.mojom.h" +#include "brave/components/ephemeral_storage/ephemeral_storage_service.h" #include "components/content_settings/core/browser/content_settings_observer.h" #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/favicon/core/favicon_driver_observer.h" @@ -86,6 +87,7 @@ class BraveShieldsTabHelper void SetHttpsUpgradeMode(mojom::HttpsUpgradeMode mode); void SetIsNoScriptEnabled(bool is_enabled); void SetForgetFirstPartyStorageEnabled(bool is_enabled); + void EnforceSiteDataCleanup(); void AllowScriptsOnce(const std::vector& origins); void BlockAllowedScripts(const std::vector& origins); void SetWebcompatEnabled(ContentSettingsType webcompat_settings_type, @@ -146,6 +148,8 @@ class BraveShieldsTabHelper const raw_ref brave_shields_settings_; PrefChangeRegistrar local_state_change_registrar_; + const raw_ptr + ephemeral_storage_service_; WEB_CONTENTS_USER_DATA_KEY_DECL(); }; diff --git a/browser/brave_shields/sources.gni b/browser/brave_shields/sources.gni index 8e7f30a1556..7e92b81c4a1 100644 --- a/browser/brave_shields/sources.gni +++ b/browser/brave_shields/sources.gni @@ -29,6 +29,7 @@ brave_browser_brave_shields_deps = [ "//brave/components/brave_shields/core/browser", "//brave/components/brave_shields/core/common", "//brave/components/constants", + "//brave/components/ephemeral_storage", "//chrome/browser/profiles:profile", "//chrome/common", "//components/content_settings/core/browser", @@ -49,4 +50,6 @@ if (is_android) { "//brave/browser/android:android_browser_process", "//brave/build/android:jni_headers", ] +} else { + brave_browser_brave_shields_deps += [ "//brave/browser/ui/tabs:tab_strip" ] } diff --git a/browser/ephemeral_storage/DEPS b/browser/ephemeral_storage/DEPS index 1fe423184f7..f0094b32863 100644 --- a/browser/ephemeral_storage/DEPS +++ b/browser/ephemeral_storage/DEPS @@ -1,6 +1,7 @@ include_rules = [ "+brave/components/ephemeral_storage", "+brave/components/brave_shields/content/test", + "+brave/build/android/jni_headers/TabUtils_jni.h", "!brave/components/brave_component_updater/browser/local_data_files_service.h", "!brave/components/brave_component_updater/browser/local_data_files_observer.h", ] diff --git a/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.cc b/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.cc index 9f063e71fb7..585db8e91ea 100644 --- a/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.cc +++ b/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.cc @@ -5,40 +5,97 @@ #include "brave/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h" +#include #include +#include #include "base/check.h" +#include "base/containers/flat_set.h" +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" #include "base/logging.h" +#include "base/memory/raw_ptr.h" #include "base/memory/scoped_refptr.h" +#include "brave/browser/brave_shields/brave_shields_settings_service_factory.h" +#include "brave/browser/ephemeral_storage/ephemeral_storage_tab_helper.h" +#include "brave/components/brave_shields/core/browser/brave_shields_settings_service.h" #include "brave/components/brave_shields/core/browser/brave_shields_utils.h" #include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h" #include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h" #include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/tabs/public/tab_interface.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browsing_data_filter_builder.h" #include "content/public/browser/browsing_data_remover.h" -#include "content/public/browser/dom_storage_context.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" #include "net/base/features.h" #include "net/base/schemeful_site.h" +#include "net/base/url_util.h" #include "url/origin.h" #if !BUILDFLAG(IS_ANDROID) +#include "brave/browser/ui/brave_browser.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#else +#include "chrome/browser/android/tab_android.h" +#include "chrome/browser/ui/android/tab_model/tab_model.h" +#include "chrome/browser/ui/android/tab_model/tab_model_list.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" #endif +namespace { + +bool PrepareTabForFirstPartyStorageCleanup(tabs::TabHandle tab_handle, + const std::string& etldplusone) { + if (tab_handle == tabs::TabHandle::Null()) { + return false; + } + auto* tab = tab_handle.Get(); + if (!tab) { + return false; + } + + content::WebContents* contents = tab->GetContents(); + if (!contents) { + return false; + } + + const auto tab_tld = + net::URLToEphemeralStorageDomain(contents->GetLastCommittedURL()); + if (tab_tld.empty() || tab_tld != etldplusone) { + return false; + } + if (auto* ephemeral_storage_tab_helper = + ephemeral_storage::EphemeralStorageTabHelper::FromWebContents( + contents)) { + ephemeral_storage_tab_helper->EnforceFirstPartyStorageCleanup(); + return true; + } + return false; +} + +} // namespace + namespace ephemeral_storage { BraveEphemeralStorageServiceDelegate::BraveEphemeralStorageServiceDelegate( content::BrowserContext* context, HostContentSettingsMap* host_content_settings_map, - scoped_refptr cookie_settings) + scoped_refptr cookie_settings, + brave_shields::BraveShieldsSettingsService* shields_settings_service) : context_(context), host_content_settings_map_(host_content_settings_map), - cookie_settings_(std::move(cookie_settings)) { + cookie_settings_(std::move(cookie_settings)), + shields_settings_service_(shields_settings_service) { DCHECK(context_); DCHECK(host_content_settings_map_); DCHECK(cookie_settings_); + CHECK(shields_settings_service_); } BraveEphemeralStorageServiceDelegate::~BraveEphemeralStorageServiceDelegate() { @@ -145,4 +202,70 @@ void BraveEphemeralStorageServiceDelegate::OnBrowserAdded(Browser* browser) { } #endif +void BraveEphemeralStorageServiceDelegate:: + PrepareTabsForFirstPartyStorageCleanup( + const std::string& ephemeral_domain) { + auto* profile = Profile::FromBrowserContext(context_); + CHECK(profile); + +#if !BUILDFLAG(IS_ANDROID) + for (auto* browser : GetAllBrowserWindowInterfaces()) { + if (profile != browser->GetProfile()) { + continue; + } + auto* tab_strip = browser->GetTabStripModel(); + if (!tab_strip) { + continue; + } + + base::flat_set tab_handlers; + for (auto* tab : *tab_strip) { + if (!tab || !PrepareTabForFirstPartyStorageCleanup(tab->GetHandle(), + ephemeral_domain)) { + continue; + } + tab_handlers.emplace(tab->GetHandle()); + } + static_cast(browser)->SetTabsToIgnoreBeforeUnloadHandlers( + tab_handlers); + + for (auto& tab_handle : tab_handlers) { + if (tab_handle == tabs::TabHandle::Null() || !tab_handle.Get()) { + continue; + } + + // initiate the closing of the tab + tab_handle.Get()->Close(); + } + } +#else + for (TabModel* model : TabModelList::models()) { + const size_t tab_count = model->GetTabCount(); + std::vector tabs_to_close; + for (size_t index = 0; index < tab_count; index++) { + auto* tab = model->GetTabAt(index); + // Do not process tabs from other profiles. + if (!tab || profile != tab->profile()) { + continue; + } + + if (!PrepareTabForFirstPartyStorageCleanup(tab->GetHandle(), + ephemeral_domain)) { + continue; + } + tabs_to_close.emplace_back(tab->GetHandle()); + } + for (auto& tab_handle : tabs_to_close) { + tab_handle.Get()->Close(); + } + } +#endif +} + +bool BraveEphemeralStorageServiceDelegate:: + IsShieldsDisabledOnAnyHostMatchingDomainOf(const GURL& url) const { + return shields_settings_service_->IsShieldsDisabledOnAnyHostMatchingDomainOf( + url); +} + } // namespace ephemeral_storage diff --git a/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h b/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h index 885f7e4d1fc..df55af0756e 100644 --- a/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h +++ b/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h @@ -6,10 +6,14 @@ #ifndef BRAVE_BROWSER_EPHEMERAL_STORAGE_BRAVE_EPHEMERAL_STORAGE_SERVICE_DELEGATE_H_ #define BRAVE_BROWSER_EPHEMERAL_STORAGE_BRAVE_EPHEMERAL_STORAGE_SERVICE_DELEGATE_H_ +#include + #include "base/memory/raw_ptr.h" #include "base/memory/scoped_refptr.h" +#include "brave/components/brave_shields/core/browser/brave_shields_settings_service.h" #include "brave/components/ephemeral_storage/ephemeral_storage_service_delegate.h" #include "components/content_settings/core/browser/cookie_settings.h" +#include "content/public/browser/web_contents_delegate.h" #if !BUILDFLAG(IS_ANDROID) #include "chrome/browser/ui/browser_list_observer.h" @@ -32,7 +36,8 @@ class BraveEphemeralStorageServiceDelegate : BraveEphemeralStorageServiceDelegate( content::BrowserContext* context, HostContentSettingsMap* host_content_settings_map, - scoped_refptr cookie_settings); + scoped_refptr cookie_settings, + brave_shields::BraveShieldsSettingsService* shields_settings_service); ~BraveEphemeralStorageServiceDelegate() override; #if !BUILDFLAG(IS_ANDROID) @@ -44,6 +49,10 @@ class BraveEphemeralStorageServiceDelegate : void CleanupTLDEphemeralArea(const TLDEphemeralAreaKey& key) override; void CleanupFirstPartyStorageArea(const TLDEphemeralAreaKey& key) override; void RegisterFirstWindowOpenedCallback(base::OnceClosure callback) override; + void PrepareTabsForFirstPartyStorageCleanup( + const std::string& ephemeral_domain) override; + bool IsShieldsDisabledOnAnyHostMatchingDomainOf( + const GURL& url) const override; private: raw_ptr context_ = nullptr; @@ -52,6 +61,8 @@ class BraveEphemeralStorageServiceDelegate : #if !BUILDFLAG(IS_ANDROID) base::OnceClosure first_window_opened_callback_; #endif // !BUILDFLAG(IS_ANDROID) + raw_ptr + shields_settings_service_ = nullptr; }; } // namespace ephemeral_storage diff --git a/browser/ephemeral_storage/ephemeral_storage_browsertest.cc b/browser/ephemeral_storage/ephemeral_storage_browsertest.cc index 2000f77af51..030425f6d06 100644 --- a/browser/ephemeral_storage/ephemeral_storage_browsertest.cc +++ b/browser/ephemeral_storage/ephemeral_storage_browsertest.cc @@ -19,6 +19,7 @@ #include "brave/browser/ephemeral_storage/ephemeral_storage_tab_helper.h" #include "brave/components/brave_shields/core/browser/brave_shields_utils.h" #include "brave/components/brave_shields/core/common/brave_shield_constants.h" +#include "brave/components/brave_shields/core/common/features.h" #include "brave/components/constants/brave_paths.h" #include "brave/components/ephemeral_storage/ephemeral_storage_service.h" #include "chrome/browser/content_settings/cookie_settings_factory.h" @@ -81,8 +82,9 @@ std::unique_ptr HandleFileRequestWithCustomHeaders( std::unique_ptr http_response; for (const auto& server_root : server_roots) { http_response = net::test_server::HandleFileRequest(server_root, request); - if (http_response) + if (http_response) { break; + } } if (http_response) { GURL request_url = request.GetURL(); @@ -170,8 +172,9 @@ bool HttpRequestMonitor::HasHttpRequestWithCookie( const std::string& cookie_value) const { base::AutoLock lock(lock_); for (const auto& http_request : http_requests_) { - if (GetHttpRequestURL(http_request) != url) + if (GetHttpRequestURL(http_request) != url) { continue; + } for (const auto& header : http_request.headers) { if (header.first == net::HttpRequestHeaders::kCookie && header.second == cookie_value) { @@ -345,15 +348,7 @@ content::EvalJsResult EphemeralStorageBrowserTest::GetCookiesInFrame( return content::EvalJs(host, "document.cookie"); } -size_t EphemeralStorageBrowserTest::WaitForCleanupAfterKeepAlive( - Profile* profile) { - if (!profile) { - profile = browser()->profile(); - } - const size_t fired_cnt = EphemeralStorageServiceFactory::GetInstance() - ->GetForContext(profile) - ->FireCleanupTimersForTesting(); - +void WaitForCleanup(Profile* profile) { // NetworkService closes existing connections when a data removal action // linked to these connections is performed. This leads to rare page open // failures when the timing is "just right". Do a no-op removal here to make @@ -365,7 +360,17 @@ size_t EphemeralStorageBrowserTest::WaitForCleanupAfterKeepAlive( &data_remover_observer); data_remover_observer.Wait(); remover->RemoveObserver(&data_remover_observer); +} +size_t EphemeralStorageBrowserTest::WaitForCleanupAfterKeepAlive( + Profile* profile) { + if (!profile) { + profile = browser()->profile(); + } + const size_t fired_cnt = EphemeralStorageServiceFactory::GetInstance() + ->GetForContext(profile) + ->FireCleanupTimersForTesting(); + WaitForCleanup(profile); return fired_cnt; } @@ -1435,3 +1440,228 @@ INSTANTIATE_TEST_SUITE_P( , EphemeralStorageWithDisableThirdPartyStoragePartitioningBrowserTest, testing::Bool()); + +class FirstPartyStorageCleanupSiteDataBrowserTest + : public EphemeralStorageBrowserTest { + public: + FirstPartyStorageCleanupSiteDataBrowserTest() { + scoped_feature_list_.InitAndEnableFeature( + brave_shields::features::kBraveShredFeature); + } + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(FirstPartyStorageCleanupSiteDataBrowserTest, + StorageIsCleanedTabsIsClosed) { + // Open two tabs with different sites. + WebContents* site_a_tab = LoadURLInNewTab(a_site_ephemeral_storage_url_); + EXPECT_TRUE(LoadURLInNewTab(a_site_ephemeral_storage_url_)); + WebContents* site_b_tab = LoadURLInNewTab(b_site_ephemeral_storage_url_); + EXPECT_EQ(browser()->tab_strip_model()->count(), 4); + EXPECT_TRUE(site_a_tab); + EXPECT_TRUE(site_b_tab); + + // Set first and third party storage values in both tabs. + SetValuesInFrames(site_a_tab, "a.com", "from=a.com"); + ValuesFromFrames site_a_tab_values = GetValuesFromFrames(site_a_tab); + EXPECT_EQ("a.com", site_a_tab_values.main_frame.local_storage); + EXPECT_EQ("a.com", site_a_tab_values.iframe_1.local_storage); + EXPECT_EQ("a.com", site_a_tab_values.iframe_2.local_storage); + SetValuesInFrames(site_b_tab, "b.com", "from=b.com"); + ValuesFromFrames site_b_tab_values = GetValuesFromFrames(site_b_tab); + EXPECT_EQ("b.com", site_b_tab_values.main_frame.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.iframe_1.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.iframe_2.local_storage); + + // Shred site data for site a.com + auto* profile = browser()->profile(); + auto storage_partition_config = + site_a_tab->GetSiteInstance()->GetStoragePartitionConfig(); + EphemeralStorageServiceFactory::GetInstance() + ->GetForContext(profile) + ->CleanupTLDFirstPartyStorage(site_a_tab->GetLastCommittedURL(), + storage_partition_config, true); + + // Wait for the cleanup to finish. + WaitForCleanupAfterKeepAlive(profile); + + // Check if the tab related to site_a_tab has been closed + int site_a_tab_index = + browser()->tab_strip_model()->GetIndexOfWebContents(site_a_tab); + EXPECT_EQ(TabStripModel::kNoTab, site_a_tab_index); + + // Open site A in new tab again and make sure that the storage is empty. + site_a_tab = LoadURLInNewTab(a_site_ephemeral_storage_url_); + auto site_a_tab_values_cleaned = GetValuesFromFrames(site_a_tab); + EXPECT_EQ(base::Value(), site_a_tab_values_cleaned.main_frame.local_storage); + EXPECT_EQ(base::Value(), site_a_tab_values_cleaned.iframe_1.local_storage); + EXPECT_EQ(base::Value(), site_a_tab_values_cleaned.iframe_2.local_storage); + + // Make sure that site B's first and third party storage data exists. + site_b_tab_values = GetValuesFromFrames(site_b_tab); + EXPECT_EQ("b.com", site_b_tab_values.main_frame.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.iframe_1.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.iframe_2.local_storage); +} + +IN_PROC_BROWSER_TEST_F(FirstPartyStorageCleanupSiteDataBrowserTest, + StorageIsCleanedTabsIsClosedForSameDomain) { + const auto a_site_ephemeral_storage_url = + https_server_.GetURL("a.com", "/ephemeral_storage.html"); + const auto a_a_site_ephemeral_storage_url = + https_server_.GetURL("a.a.com", "/ephemeral_storage.html"); + const auto b_a_site_ephemeral_storage_url = + https_server_.GetURL("b.a.com", "/ephemeral_storage.html"); + + // Open tabs with different sites - a.com, a.a.com, b.a.com, and b.com + WebContents* site_a_tab = LoadURLInNewTab(a_site_ephemeral_storage_url); + WebContents* site_a_a_tab = LoadURLInNewTab(a_a_site_ephemeral_storage_url); + WebContents* site_b_a_tab = LoadURLInNewTab(b_a_site_ephemeral_storage_url); + WebContents* site_b_tab = LoadURLInNewTab(b_site_ephemeral_storage_url_); + + EXPECT_EQ(browser()->tab_strip_model()->count(), 5); + EXPECT_TRUE(site_a_tab); + EXPECT_TRUE(site_a_a_tab); + EXPECT_TRUE(site_b_a_tab); + EXPECT_TRUE(site_b_tab); + + // Set storage values in main frames only (to avoid iframe complexity) + // We're primarily testing tab closure behavior, not iframe partitioning + RenderFrameHost* site_a_main = site_a_tab->GetPrimaryMainFrame(); + RenderFrameHost* site_a_a_main = site_a_a_tab->GetPrimaryMainFrame(); + RenderFrameHost* site_b_a_main = site_b_a_tab->GetPrimaryMainFrame(); + RenderFrameHost* site_b_main = site_b_tab->GetPrimaryMainFrame(); + + SetValuesInFrame(site_a_main, "a.com", "from=a.com"); + SetValuesInFrame(site_a_a_main, "a.a.com", "from=a.a.com"); + SetValuesInFrame(site_b_a_main, "b.a.com", "from=b.a.com"); + SetValuesInFrame(site_b_main, "b.com", "from=b.com"); + + // Verify that storage values are set correctly before shredding + ValuesFromFrame site_a_tab_values = GetValuesFromFrame(site_a_main); + ValuesFromFrame site_a_a_tab_values = GetValuesFromFrame(site_a_a_main); + ValuesFromFrame site_b_a_tab_values = GetValuesFromFrame(site_b_a_main); + ValuesFromFrame site_b_tab_values = GetValuesFromFrame(site_b_main); + + EXPECT_EQ("a.com", site_a_tab_values.local_storage); + EXPECT_EQ("a.com", site_a_tab_values.session_storage); + EXPECT_EQ("from=a.com", site_a_tab_values.cookies); + + EXPECT_EQ("a.a.com", site_a_a_tab_values.local_storage); + EXPECT_EQ("a.a.com", site_a_a_tab_values.session_storage); + EXPECT_EQ("from=a.a.com", site_a_a_tab_values.cookies); + + EXPECT_EQ("b.a.com", site_b_a_tab_values.local_storage); + EXPECT_EQ("b.a.com", site_b_a_tab_values.session_storage); + EXPECT_EQ("from=b.a.com", site_b_a_tab_values.cookies); + + EXPECT_EQ("b.com", site_b_tab_values.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.session_storage); + EXPECT_EQ("from=b.com", site_b_tab_values.cookies); + + // Shred site data for a.com - this should close tabs for a.com, a.a.com, and + // b.a.com + auto* profile = browser()->profile(); + auto storage_partition_config = + site_a_tab->GetSiteInstance()->GetStoragePartitionConfig(); + EphemeralStorageServiceFactory::GetInstance() + ->GetForContext(profile) + ->CleanupTLDFirstPartyStorage(site_a_tab->GetLastCommittedURL(), + storage_partition_config, true); + + // Wait for the cleanup to finish. + WaitForCleanupAfterKeepAlive(profile); + + // Check that tabs for a.com, a.a.com, and b.a.com have been closed + EXPECT_EQ(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_a_tab)); + EXPECT_EQ(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_a_a_tab)); + EXPECT_EQ(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_b_a_tab)); + + // Make sure that b.com remains open + EXPECT_NE(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_b_tab)); + + // Verify that b.com tab still has its data intact + site_b_tab_values = GetValuesFromFrame(site_b_main); + EXPECT_EQ("b.com", site_b_tab_values.local_storage); + EXPECT_EQ("b.com", site_b_tab_values.session_storage); + EXPECT_EQ("from=b.com", site_b_tab_values.cookies); + + // Open new tabs for the shredded domains and verify their storage is empty + WebContents* new_site_a_tab = LoadURLInNewTab(a_site_ephemeral_storage_url); + WebContents* new_site_a_a_tab = + LoadURLInNewTab(a_a_site_ephemeral_storage_url); + WebContents* new_site_b_a_tab = + LoadURLInNewTab(b_a_site_ephemeral_storage_url); + + // All shredded domains should have empty storage + ExpectValuesFromFrameAreEmpty( + FROM_HERE, GetValuesFromFrame(new_site_a_tab->GetPrimaryMainFrame())); + ExpectValuesFromFrameAreEmpty( + FROM_HERE, GetValuesFromFrame(new_site_a_a_tab->GetPrimaryMainFrame())); + ExpectValuesFromFrameAreEmpty( + FROM_HERE, GetValuesFromFrame(new_site_b_a_tab->GetPrimaryMainFrame())); +} + +IN_PROC_BROWSER_TEST_F(FirstPartyStorageCleanupSiteDataBrowserTest, + DoNotCleanupStorageIfShieldsDisabledOnAnySubdomain) { + const auto a_site_ephemeral_storage_url = + https_server_.GetURL("a.com", "/ephemeral_storage.html"); + const auto a_a_site_ephemeral_storage_url = + https_server_.GetURL("a.a.com", "/ephemeral_storage.html"); + + brave_shields::SetBraveShieldsEnabled(content_settings(), false, + a_a_site_ephemeral_storage_url); + + // Open tabs with different sites - a.com, a.a.com, b.a.com, and b.com + WebContents* site_a_tab = LoadURLInNewTab(a_site_ephemeral_storage_url); + WebContents* site_a_a_tab = LoadURLInNewTab(a_a_site_ephemeral_storage_url); + + EXPECT_EQ(browser()->tab_strip_model()->count(), 3); + EXPECT_TRUE(site_a_tab); + EXPECT_TRUE(site_a_a_tab); + + // Set storage values in main frames only (to avoid iframe complexity) + // We're primarily testing tab closure behavior, not iframe partitioning + RenderFrameHost* site_a_main = site_a_tab->GetPrimaryMainFrame(); + RenderFrameHost* site_a_a_main = site_a_a_tab->GetPrimaryMainFrame(); + + SetValuesInFrame(site_a_main, "a.com", "from=a.com"); + SetValuesInFrame(site_a_a_main, "a.a.com", "from=a.a.com"); + + // Verify that storage values are set correctly before shredding + ValuesFromFrame site_a_tab_values = GetValuesFromFrame(site_a_main); + ValuesFromFrame site_a_a_tab_values = GetValuesFromFrame(site_a_a_main); + + EXPECT_EQ("a.com", site_a_tab_values.local_storage); + EXPECT_EQ("a.com", site_a_tab_values.session_storage); + EXPECT_EQ("from=a.com", site_a_tab_values.cookies); + + EXPECT_EQ("a.a.com", site_a_a_tab_values.local_storage); + EXPECT_EQ("a.a.com", site_a_a_tab_values.session_storage); + EXPECT_EQ("from=a.a.com", site_a_a_tab_values.cookies); + + // Try to Shred site data for a.com - this should not be executed as shields + // are disabled on a.a.com + auto* profile = browser()->profile(); + auto storage_partition_config = + site_a_tab->GetSiteInstance()->GetStoragePartitionConfig(); + EphemeralStorageServiceFactory::GetInstance() + ->GetForContext(profile) + ->CleanupTLDFirstPartyStorage(site_a_tab->GetLastCommittedURL(), + storage_partition_config, false); + + // Wait for the cleanup to finish. + WaitForCleanupAfterKeepAlive(profile); + + // Check that tabs for a.com, a.a.com, and b.a.com have not been closed + EXPECT_NE(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_a_tab)); + EXPECT_NE(TabStripModel::kNoTab, + browser()->tab_strip_model()->GetIndexOfWebContents(site_a_a_tab)); +} diff --git a/browser/ephemeral_storage/ephemeral_storage_service_factory.cc b/browser/ephemeral_storage/ephemeral_storage_service_factory.cc index 7c1c65d1d5f..7bbd0c0a71d 100644 --- a/browser/ephemeral_storage/ephemeral_storage_service_factory.cc +++ b/browser/ephemeral_storage/ephemeral_storage_service_factory.cc @@ -9,6 +9,7 @@ #include "base/feature_list.h" #include "base/no_destructor.h" +#include "brave/browser/brave_shields/brave_shields_settings_service_factory.h" #include "brave/browser/ephemeral_storage/brave_ephemeral_storage_service_delegate.h" #include "brave/components/ephemeral_storage/ephemeral_storage_pref_names.h" #include "brave/components/ephemeral_storage/ephemeral_storage_service.h" @@ -40,6 +41,7 @@ EphemeralStorageServiceFactory::EphemeralStorageServiceFactory() BrowserContextDependencyManager::GetInstance()) { DependsOn(HostContentSettingsMapFactory::GetInstance()); DependsOn(CookieSettingsFactory::GetInstance()); + DependsOn(BraveShieldsSettingsServiceFactory::GetInstance()); } EphemeralStorageServiceFactory::~EphemeralStorageServiceFactory() = default; @@ -72,7 +74,8 @@ EphemeralStorageServiceFactory::BuildServiceInstanceForBrowserContext( context, host_content_settings_map, std::make_unique( context, host_content_settings_map, - CookieSettingsFactory::GetForProfile(profile))); + CookieSettingsFactory::GetForProfile(profile), + BraveShieldsSettingsServiceFactory::GetForProfile(profile))); } content::BrowserContext* EphemeralStorageServiceFactory::GetBrowserContextToUse( diff --git a/browser/ephemeral_storage/ephemeral_storage_service_unittest.cc b/browser/ephemeral_storage/ephemeral_storage_service_unittest.cc index b0dae2a9c78..aed1ce3f108 100644 --- a/browser/ephemeral_storage/ephemeral_storage_service_unittest.cc +++ b/browser/ephemeral_storage/ephemeral_storage_service_unittest.cc @@ -5,6 +5,7 @@ #include "brave/components/ephemeral_storage/ephemeral_storage_service.h" +#include #include #include "base/test/scoped_feature_list.h" @@ -52,6 +53,14 @@ class MockDelegate : public EphemeralStorageServiceDelegate { RegisterFirstWindowOpenedCallback, (base::OnceClosure callback), (override)); + MOCK_METHOD(bool, + IsShieldsDisabledOnAnyHostMatchingDomainOf, + (const GURL& url), + (const override)); + MOCK_METHOD(void, + PrepareTabsForFirstPartyStorageCleanup, + (const std::string& ephemeral_domain), + (override)); void ExpectRegisterFirstWindowOpenedCallback(base::OnceClosure callback, bool trigger_callback) { @@ -161,8 +170,8 @@ TEST_F(EphemeralStorageServiceTest, EphemeralCleanup) { { ScopedVerifyAndClearExpectations verify(mock_delegate_); ScopedVerifyAndClearExpectations verify_observer(&mock_observer_); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); task_environment_.FastForwardBy(base::Seconds(10)); } @@ -174,8 +183,8 @@ TEST_F(EphemeralStorageServiceTest, EphemeralCleanup) { { ScopedVerifyAndClearExpectations verify(mock_delegate_); ScopedVerifyAndClearExpectations verify_observer(&mock_observer_); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); task_environment_.FastForwardBy(base::Seconds(20)); } @@ -213,8 +222,8 @@ TEST_F(EphemeralStorageServiceTest, TLDEphemeralAreaKey key(ephemeral_domain, storage_partition_config); EXPECT_CALL(mock_observer_, OnCleanupTLDEphemeralArea(key)); EXPECT_CALL(*mock_delegate_, CleanupTLDEphemeralArea(key)); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); task_environment_.FastForwardBy(base::Seconds(30)); } @@ -226,7 +235,7 @@ TEST_F(EphemeralStorageServiceTest, EXPECT_CALL(mock_observer_, OnCleanupTLDEphemeralArea(key)); EXPECT_CALL(*mock_delegate_, CleanupTLDEphemeralArea(key)); service_->TLDEphemeralLifetimeDestroyed( - ephemeral_domain, second_storage_partition_config, false); + ephemeral_domain, second_storage_partition_config, false, false); task_environment_.FastForwardBy(base::Seconds(30)); } } @@ -258,8 +267,8 @@ TEST_F(EphemeralStorageServiceNoKeepAliveTest, ImmediateCleanup) { TLDEphemeralAreaKey key(ephemeral_domain, storage_partition_config); EXPECT_CALL(mock_observer_, OnCleanupTLDEphemeralArea(key)); EXPECT_CALL(*mock_delegate_, CleanupTLDEphemeralArea(key)); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); } } @@ -323,9 +332,9 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, CleanupFirstPartyStorage) { .Times(test_case.shields_enabled); EXPECT_CALL(*mock_delegate_, CleanupFirstPartyStorageArea(key)) .Times(test_case.should_cleanup); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, - !test_case.shields_enabled); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, + !test_case.shields_enabled, false); EXPECT_EQ(profile_.GetPrefs() ->GetList(kFirstPartyStorageOriginsToCleanup) .size(), @@ -360,8 +369,8 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, CleanupOnRestart) { { ScopedVerifyAndClearExpectations verify(mock_delegate_); ScopedVerifyAndClearExpectations verify_observer(&mock_observer_); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); EXPECT_EQ( profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 1u); @@ -411,8 +420,8 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 0u); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); EXPECT_EQ( profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 1u); @@ -463,10 +472,10 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 0u); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); service_->TLDEphemeralLifetimeDestroyed( - ephemeral_domain, second_storage_partition_config, false); + ephemeral_domain, storage_partition_config, false, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, second_storage_partition_config, false, false); EXPECT_EQ( profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 2u); @@ -523,8 +532,8 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, { ScopedVerifyAndClearExpectations verify(mock_delegate_); ScopedVerifyAndClearExpectations verify_observer(&mock_observer_); - service_->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + service_->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); EXPECT_EQ( profile_.GetPrefs()->GetList(kFirstPartyStorageOriginsToCleanup).size(), 1u); @@ -590,8 +599,8 @@ TEST_F(EphemeralStorageServiceForgetFirstPartyTest, OffTheRecordSkipsPrefs) { .size(), 0u); - otr_service->TLDEphemeralLifetimeDestroyed(ephemeral_domain, - storage_partition_config, false); + otr_service->TLDEphemeralLifetimeDestroyed( + ephemeral_domain, storage_partition_config, false, false); EXPECT_EQ(otr_profile->GetPrefs() ->GetList(kFirstPartyStorageOriginsToCleanup) .size(), diff --git a/browser/ephemeral_storage/ephemeral_storage_tab_helper.cc b/browser/ephemeral_storage/ephemeral_storage_tab_helper.cc index 8db9dbdd201..701f816d723 100644 --- a/browser/ephemeral_storage/ephemeral_storage_tab_helper.cc +++ b/browser/ephemeral_storage/ephemeral_storage_tab_helper.cc @@ -60,6 +60,12 @@ EphemeralStorageTabHelper::GetEphemeralStorageToken(const url::Origin& origin) { return std::nullopt; } +void EphemeralStorageTabHelper::EnforceFirstPartyStorageCleanup() { + if (tld_ephemeral_lifetime_) { + tld_ephemeral_lifetime_->EnforceFirstPartyStorageCleanup(); + } +} + void EphemeralStorageTabHelper::WebContentsDestroyed() { provisional_tld_ephemeral_lifetimes_.clear(); tld_ephemeral_lifetime_.reset(); diff --git a/browser/ephemeral_storage/ephemeral_storage_tab_helper.h b/browser/ephemeral_storage/ephemeral_storage_tab_helper.h index 7d92b530753..11861a7b12d 100644 --- a/browser/ephemeral_storage/ephemeral_storage_tab_helper.h +++ b/browser/ephemeral_storage/ephemeral_storage_tab_helper.h @@ -43,6 +43,8 @@ class EphemeralStorageTabHelper std::optional GetEphemeralStorageToken( const url::Origin& origin); + void EnforceFirstPartyStorageCleanup(); + private: friend class content::WebContentsUserData; diff --git a/browser/ephemeral_storage/tld_ephemeral_lifetime.cc b/browser/ephemeral_storage/tld_ephemeral_lifetime.cc index c3368a799b9..73d109eaa9a 100644 --- a/browser/ephemeral_storage/tld_ephemeral_lifetime.cc +++ b/browser/ephemeral_storage/tld_ephemeral_lifetime.cc @@ -49,12 +49,17 @@ TLDEphemeralLifetime::~TLDEphemeralLifetime() { shields_state_on_hosts_, [](const auto& v) { return !v.second; }); ephemeral_storage_service_->TLDEphemeralLifetimeDestroyed( key_.storage_domain, key_.storage_partition_config, - shields_disabled_on_one_of_hosts); + shields_disabled_on_one_of_hosts, + first_party_storage_cleanup_enforced_); } ActiveTLDStorageAreas().erase(key_); } +void TLDEphemeralLifetime::EnforceFirstPartyStorageCleanup() { + first_party_storage_cleanup_enforced_ = true; +} + // static TLDEphemeralLifetime* TLDEphemeralLifetime::Get( const TLDEphemeralLifetimeKey& key) { diff --git a/browser/ephemeral_storage/tld_ephemeral_lifetime.h b/browser/ephemeral_storage/tld_ephemeral_lifetime.h index fe09dcfe051..dacf7cc5c2e 100644 --- a/browser/ephemeral_storage/tld_ephemeral_lifetime.h +++ b/browser/ephemeral_storage/tld_ephemeral_lifetime.h @@ -57,6 +57,7 @@ class TLDEphemeralLifetime : public base::RefCounted { const TLDEphemeralLifetimeKey& key() const { return key_; } void SetShieldsStateOnHost(std::string_view host, bool enabled); + void EnforceFirstPartyStorageCleanup(); private: friend class RefCounted; @@ -65,6 +66,7 @@ class TLDEphemeralLifetime : public base::RefCounted { TLDEphemeralLifetimeKey key_; base::WeakPtr ephemeral_storage_service_; absl::flat_hash_map shields_state_on_hosts_; + bool first_party_storage_cleanup_enforced_{false}; base::WeakPtrFactory weak_factory_{this}; }; diff --git a/browser/ui/brave_browser.cc b/browser/ui/brave_browser.cc index a038086f7a0..a0fbedf4b64 100644 --- a/browser/ui/brave_browser.cc +++ b/browser/ui/brave_browser.cc @@ -12,6 +12,7 @@ #include "base/check.h" #include "base/check_is_test.h" +#include "base/containers/contains.h" #include "base/feature_list.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" @@ -47,6 +48,7 @@ #include "components/tabs/public/tab_interface.h" #include "content/public/browser/file_select_listener.h" #include "content/public/browser/navigation_entry.h" +#include "content/public/browser/web_contents_delegate.h" #include "content/public/common/url_constants.h" #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" #include "url/gurl.h" @@ -233,6 +235,15 @@ void BraveBrowser::OnTabStripModelChanged( selection.active_tab_changed()) { sidebar_controller->sidebar()->UpdateSidebarItemsState(); } + + // Check if all tabs we set to ignore onbeforeunload handler are closed. + if (!tabs_closing_with_onbeforeunload_ignore_.empty() && + change.type() == TabStripModelChange::Type::kRemoved) { + for (auto& removed_tab : change.GetRemove()->contents) { + tabs_closing_with_onbeforeunload_ignore_.erase( + removed_tab.tab->GetHandle()); + } + } } void BraveBrowser::FinishWarnBeforeClosing(WarnBeforeClosingResult result) { @@ -359,3 +370,15 @@ bool BraveBrowser::AreAllTabsSharedPinnedTabs() { BraveBrowserWindow* BraveBrowser::brave_window() { return static_cast(window_.get()); } + +void BraveBrowser::SetTabsToIgnoreBeforeUnloadHandlers( + const base::flat_set& for_contents) { + tabs_closing_with_onbeforeunload_ignore_ = for_contents; +} + +bool BraveBrowser::ShouldSuppressDialogs(content::WebContents* source) { + auto* tab = tabs::TabInterface::MaybeGetFromContents(source); + return (tab && tabs_closing_with_onbeforeunload_ignore_.contains( + tab->GetHandle())) || + content::WebContentsDelegate::ShouldSuppressDialogs(source); +} diff --git a/browser/ui/brave_browser.h b/browser/ui/brave_browser.h index 18036c5ebda..a6873ecf785 100644 --- a/browser/ui/brave_browser.h +++ b/browser/ui/brave_browser.h @@ -8,6 +8,7 @@ #include +#include "base/containers/flat_set.h" #include "base/memory/scoped_refptr.h" #include "base/memory/weak_ptr.h" #include "chrome/browser/ui/browser.h" @@ -69,6 +70,10 @@ class BraveBrowser : public Browser { // any warning/onbeforeunload handlers. bool ShouldAskForBrowserClosingBeforeHandlers(); + // Allows ignoring onbeforeunload handlers when closing selected tabs. + void SetTabsToIgnoreBeforeUnloadHandlers( + const base::flat_set& for_contents); + BraveBrowserWindow* brave_window(); void set_confirmed_to_close(bool close) { confirmed_to_close_ = close; } @@ -87,6 +92,8 @@ class BraveBrowser : public Browser { bool AreAllTabsSharedPinnedTabs(); + bool ShouldSuppressDialogs(content::WebContents* source) override; + // Set true when user allowed to close browser before starting any // warning or onbeforeunload handlers. bool confirmed_to_close_ = false; @@ -96,6 +103,9 @@ class BraveBrowser : public Browser { // new tab, like when user tries to "Bring all tabs" to other window. bool ignore_enable_closing_last_tab_pref_ = false; + // WebContents for which onbeforeunload handlers should be ignored. + base::flat_set tabs_closing_with_onbeforeunload_ignore_; + base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/brave_shields/core/browser/brave_shields_settings_service.cc b/components/brave_shields/core/browser/brave_shields_settings_service.cc index 8a8e43ae96d..7b66186fa9d 100644 --- a/components/brave_shields/core/browser/brave_shields_settings_service.cc +++ b/components/brave_shields/core/browser/brave_shields_settings_service.cc @@ -14,6 +14,7 @@ #include "components/content_settings/core/common/content_settings_pattern.h" #include "components/content_settings/core/common/content_settings_types.h" #include "components/prefs/pref_service.h" +#include "net/base/url_util.h" #include "url/gurl.h" namespace brave_shields { @@ -256,4 +257,42 @@ BraveShieldsSettingsService::GetJsContentSettingOverriddenData( rule, ConvertSettingsSource(info.source)); } +bool BraveShieldsSettingsService::IsShieldsDisabledOnAnyHostMatchingDomainOf( + const GURL& url) const { + // First check the exact domain + if (CONTENT_SETTING_BLOCK == + host_content_settings_map_->GetContentSetting( + url, GURL(), ContentSettingsType::BRAVE_SHIELDS)) { + return true; + } + + // Check parent domains by iterating through all shield settings + ContentSettingsForOneType all_shield_settings = + host_content_settings_map_->GetSettingsForOneType( + ContentSettingsType::BRAVE_SHIELDS); + + const auto ephemeral_domain = net::URLToEphemeralStorageDomain(url); + + for (const auto& setting : all_shield_settings) { + // Skip invalid patterns or settings that don't disable shields + if (!setting.primary_pattern.IsValid() || + setting.setting_value != CONTENT_SETTING_BLOCK) { + continue; + } + + // Skip wildcard patterns that match all hosts + if (setting.primary_pattern.MatchesAllHosts()) { + return true; + } + + if (const GURL pattern_url(setting.primary_pattern.ToRepresentativeUrl()); + pattern_url.is_valid() && + ephemeral_domain == net::URLToEphemeralStorageDomain(pattern_url)) { + return true; + } + } + + return false; +} + } // namespace brave_shields diff --git a/components/brave_shields/core/browser/brave_shields_settings_service.h b/components/brave_shields/core/browser/brave_shields_settings_service.h index efe0b44504a..6182ba29a7f 100644 --- a/components/brave_shields/core/browser/brave_shields_settings_service.h +++ b/components/brave_shields/core/browser/brave_shields_settings_service.h @@ -63,6 +63,8 @@ class BraveShieldsSettingsService : public KeyedService { mojom::ContentSettingsOverriddenDataPtr GetJsContentSettingOverriddenData( const GURL& url); + bool IsShieldsDisabledOnAnyHostMatchingDomainOf(const GURL& url) const; + private: const raw_ref host_content_settings_map_; // NOT OWNED diff --git a/components/brave_shields/core/browser/brave_shields_settings_service_unittest.cc b/components/brave_shields/core/browser/brave_shields_settings_service_unittest.cc index 4462535e045..452bb7b909d 100644 --- a/components/brave_shields/core/browser/brave_shields_settings_service_unittest.cc +++ b/components/brave_shields/core/browser/brave_shields_settings_service_unittest.cc @@ -432,3 +432,186 @@ TEST_F(BraveShieldsSettingsServiceTest, CONTENT_SETTING_BLOCK); } #endif + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_NoSettings) { + // With no shield settings, should return false + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_ExactDomainMatch) { + // Tests exact domain matching + const auto example_com_url = GURL("https://example.com"); + brave_shields_settings()->SetBraveShieldsEnabled(false, example_com_url); + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_WildcardPattern) { + // Set shields to BLOCK for wildcard pattern + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("[*.]example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_SubdomainPattern) { + // Tests specific subdomain patterns + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("sub.example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_AllowSettingIgnored) { + // Set shields to ALLOW (not BLOCK) - should be ignored + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_ALLOW); + + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_AllHostsPatternIgnored) { + // Test whildcard hosts pattern + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::Wildcard(), ContentSettingsPattern::Wildcard(), + ContentSettingsType::BRAVE_SHIELDS, CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_MultipleSettings) { + // Set multiple settings - some matching, some not + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("test.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("other.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_ALLOW); + + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("[*.]example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://other.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://unrelated.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_DifferentSubdomains) { + // Set shields to BLOCK for specific subdomain + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("mail.test.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + // Should match for test.com ephemeral domain + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_PortsInPatterns) { + // Test with patterns that include ports + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("example.com:8080"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com:8080"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_MixedSchemePatterns) { + // Test with different scheme patterns + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("http://example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("https://test.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://test.com"))); +} + +TEST_F(BraveShieldsSettingsServiceTest, + IsShieldsDisabledOnAnyHostMatchingDomainOf_DeepSubdomainHierarchy) { + // Test with deeply nested subdomains + GetHostContentSettingsMap()->SetContentSettingCustomScope( + ContentSettingsPattern::FromString("a.b.c.d.example.com"), + ContentSettingsPattern::Wildcard(), ContentSettingsType::BRAVE_SHIELDS, + CONTENT_SETTING_BLOCK); + + EXPECT_TRUE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://example.com"))); + EXPECT_FALSE( + brave_shields_settings()->IsShieldsDisabledOnAnyHostMatchingDomainOf( + GURL("https://other.com"))); +} diff --git a/components/ephemeral_storage/BUILD.gn b/components/ephemeral_storage/BUILD.gn index 1749037d4fa..61c15f8a5bb 100644 --- a/components/ephemeral_storage/BUILD.gn +++ b/components/ephemeral_storage/BUILD.gn @@ -17,6 +17,7 @@ static_library("ephemeral_storage") { deps = [ "//base", + "//brave/components/brave_shields/core/common", "//components/content_settings/core/browser", "//components/keyed_service/core", "//components/prefs", diff --git a/components/ephemeral_storage/ephemeral_storage_service.cc b/components/ephemeral_storage/ephemeral_storage_service.cc index 34954e576a2..72099dd761c 100644 --- a/components/ephemeral_storage/ephemeral_storage_service.cc +++ b/components/ephemeral_storage/ephemeral_storage_service.cc @@ -10,11 +10,13 @@ #include #include "base/check.h" +#include "base/functional/bind.h" #include "base/logging.h" #include "base/strings/strcat.h" #include "base/task/sequenced_task_runner.h" #include "base/time/time.h" #include "base/timer/timer.h" +#include "brave/components/brave_shields/core/common/features.h" #include "brave/components/ephemeral_storage/ephemeral_storage_pref_names.h" #include "brave/components/ephemeral_storage/url_storage_checker.h" #include "components/content_settings/core/browser/host_content_settings_map.h" @@ -23,6 +25,7 @@ #include "content/public/browser/browser_context.h" #include "content/public/browser/site_instance.h" #include "content/public/browser/storage_partition_config.h" +#include "content/public/browser/web_contents.h" #include "net/base/features.h" #include "net/base/schemeful_site.h" #include "net/base/url_util.h" @@ -221,20 +224,26 @@ void EphemeralStorageService::TLDEphemeralLifetimeCreated( void EphemeralStorageService::TLDEphemeralLifetimeDestroyed( const std::string& ephemeral_domain, const content::StoragePartitionConfig& storage_partition_config, - bool shields_disabled_on_one_of_hosts) { + bool shields_disabled_on_one_of_hosts, + bool first_party_storage_cleanup_enforced) { DVLOG(1) << __func__ << " " << ephemeral_domain << " " << storage_partition_config; const TLDEphemeralAreaKey key(ephemeral_domain, storage_partition_config); - const bool cleanup_tld_ephemeral_area = !shields_disabled_on_one_of_hosts; + const bool cleanup_tld_ephemeral_area = + !shields_disabled_on_one_of_hosts || first_party_storage_cleanup_enforced; const bool cleanup_first_party_storage_area = FirstPartyStorageAreaNotInUse(ephemeral_domain, storage_partition_config, - shields_disabled_on_one_of_hosts); + shields_disabled_on_one_of_hosts) || + first_party_storage_cleanup_enforced; - if (base::FeatureList::IsEnabled( + if (first_party_storage_cleanup_enforced || + base::FeatureList::IsEnabled( net::features::kBraveEphemeralStorageKeepAlive)) { auto cleanup_timer = std::make_unique(); cleanup_timer->Start( - FROM_HERE, tld_ephemeral_area_keep_alive_, + FROM_HERE, + first_party_storage_cleanup_enforced ? base::Milliseconds(500) + : tld_ephemeral_area_keep_alive_, base::BindOnce(&EphemeralStorageService::CleanupTLDEphemeralAreaByTimer, weak_ptr_factory_.GetWeakPtr(), key, cleanup_tld_ephemeral_area, @@ -256,6 +265,27 @@ void EphemeralStorageService::RemoveObserver( observer_list_.RemoveObserver(observer); } +void EphemeralStorageService::CleanupTLDFirstPartyStorage( + const GURL& url, + const content::StoragePartitionConfig& storage_partition_config, + const bool enforced_by_user) { + if (!base::FeatureList::IsEnabled( + brave_shields::features::kBraveShredFeature)) { + return; + } + + if (!enforced_by_user && + delegate_->IsShieldsDisabledOnAnyHostMatchingDomainOf(url)) { + // Do not start auto shred if shields is disabled on any host matching the + // domain or ephemeral_domain is empty. + return; + } + + const auto ephemeral_domain = net::URLToEphemeralStorageDomain(url); + delegate_->PrepareTabsForFirstPartyStorageCleanup( + std::move(ephemeral_domain)); +} + void EphemeralStorageService::FirstPartyStorageAreaInUse( const std::string& ephemeral_domain, const content::StoragePartitionConfig& storage_partition_config) { diff --git a/components/ephemeral_storage/ephemeral_storage_service.h b/components/ephemeral_storage/ephemeral_storage_service.h index ab0a15d3c2f..9321bf4b198 100644 --- a/components/ephemeral_storage/ephemeral_storage_service.h +++ b/components/ephemeral_storage/ephemeral_storage_service.h @@ -77,11 +77,17 @@ class EphemeralStorageService : public KeyedService { void TLDEphemeralLifetimeDestroyed( const std::string& ephemeral_domain, const content::StoragePartitionConfig& storage_partition_config, - bool shields_disabled_on_one_of_hosts); + bool shields_disabled_on_one_of_hosts, + bool first_party_storage_cleanup_enforced); void AddObserver(EphemeralStorageServiceObserver* observer); void RemoveObserver(EphemeralStorageServiceObserver* observer); + void CleanupTLDFirstPartyStorage( + const GURL& url, + const content::StoragePartitionConfig& storage_partition_config, + const bool enforced_by_user); + private: friend EphemeralStorageBrowserTest; friend EphemeralStorageQaBrowserTest; diff --git a/components/ephemeral_storage/ephemeral_storage_service_delegate.h b/components/ephemeral_storage/ephemeral_storage_service_delegate.h index 3cd341c53f6..58703aebb33 100644 --- a/components/ephemeral_storage/ephemeral_storage_service_delegate.h +++ b/components/ephemeral_storage/ephemeral_storage_service_delegate.h @@ -6,8 +6,11 @@ #ifndef BRAVE_COMPONENTS_EPHEMERAL_STORAGE_EPHEMERAL_STORAGE_SERVICE_DELEGATE_H_ #define BRAVE_COMPONENTS_EPHEMERAL_STORAGE_EPHEMERAL_STORAGE_SERVICE_DELEGATE_H_ +#include + #include "base/functional/callback.h" #include "brave/components/ephemeral_storage/ephemeral_storage_types.h" +#include "url/gurl.h" namespace ephemeral_storage { @@ -23,6 +26,10 @@ class EphemeralStorageServiceDelegate { // Registers a callback to be called when the first window is opened. virtual void RegisterFirstWindowOpenedCallback( base::OnceClosure callback) = 0; + virtual void PrepareTabsForFirstPartyStorageCleanup( + const std::string& ephemeral_domain) = 0; + virtual bool IsShieldsDisabledOnAnyHostMatchingDomainOf( + const GURL& url) const = 0; }; } // namespace ephemeral_storage