| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <cstddef> |
| #include <optional> |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/blocked_content/framebust_block_tab_helper.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_content_setting_bubble_model_delegate.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_features.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h" |
| #include "chrome/browser/ui/content_settings/fake_owner.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/blocked_content/url_list_manager.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/isolated_world_ids.h" |
| #include "content/public/common/url_constants.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_navigation_observer.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/event.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/system_web_apps/system_web_app_manager.h" |
| #endif |
| |
| namespace { |
| |
| const int kAllowRadioButtonIndex = 0; |
| const int kDisallowRadioButtonIndex = 1; |
| |
| } // namespace |
| |
| class FramebustBlockBrowserTest |
| : public InProcessBrowserTest, |
| public blocked_content::UrlListManager::Observer { |
| public: |
| FramebustBlockBrowserTest() = default; |
| |
| // InProcessBrowserTest: |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| current_browser_ = InProcessBrowserTest::browser(); |
| FramebustBlockTabHelper::FromWebContents(GetWebContents()) |
| ->manager() |
| ->AddObserver(this); |
| } |
| |
| // UrlListManager::Observer: |
| void BlockedUrlAdded(int32_t id, const GURL& blocked_url) override { |
| if (!blocked_url_added_closure_.is_null()) { |
| std::move(blocked_url_added_closure_).Run(); |
| } |
| } |
| |
| content::WebContents* GetWebContents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| FramebustBlockTabHelper* GetFramebustTabHelper() { |
| return FramebustBlockTabHelper::FromWebContents(GetWebContents()); |
| } |
| |
| void OnClick(const GURL& url, size_t index, size_t total_size) { |
| clicked_url_ = url; |
| clicked_index_ = index; |
| } |
| |
| Browser* browser() { return current_browser_; } |
| |
| void CreateAndSetBrowser() { |
| current_browser_ = CreateBrowser(browser()->profile()); |
| } |
| |
| bool NavigateIframeToUrlWithoutGesture(content::WebContents* contents, |
| const std::string iframe_id, |
| const GURL& url) { |
| const char kScript[] = R"( |
| var iframe = document.getElementById('%s'); |
| iframe.src='%s' |
| )"; |
| content::TestNavigationObserver load_observer(contents); |
| bool result = content::ExecJs( |
| contents, |
| base::StringPrintf(kScript, iframe_id.c_str(), url.spec().c_str()), |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| load_observer.Wait(); |
| return result; |
| } |
| |
| bool ExecuteAndCheckBlockedRedirection() { |
| return ExecuteAndCheckBlockedRedirection( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| } |
| |
| // Attempts to framebust to `redirect_url` and ensures the navigation is |
| // blocked. (The test fails if not.) Returns whether the blocked URL is added |
| // to the tab helper, where the user can proceed to it if desired. |
| bool ExecuteAndCheckBlockedRedirection(const GURL& redirect_url) { |
| const GURL original_url = embedded_test_server()->GetURL("/iframe.html"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url)); |
| |
| const GURL child_url = |
| embedded_test_server()->GetURL("a.com", "/title1.html"); |
| NavigateIframeToUrlWithoutGesture(GetWebContents(), "test", child_url); |
| |
| content::RenderFrameHost* child = |
| content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0); |
| EXPECT_EQ(child_url, child->GetLastCommittedURL()); |
| |
| base::RunLoop block_waiter; |
| blocked_url_added_closure_ = block_waiter.QuitClosure(); |
| child->ExecuteJavaScriptForTests( |
| base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';", |
| redirect_url.spec().c_str())), |
| base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL); |
| block_waiter.Run(); |
| |
| // Ensure we have not left the original page. |
| EXPECT_EQ(original_url, GetWebContents()->GetLastCommittedURL()); |
| |
| // Return whether the redirect URL itself ended up in the list of blocked |
| // URLs, which only happens if the renderer had the ability to navigate to |
| // the URL in the first place. |
| return base::Contains(GetFramebustTabHelper()->blocked_urls(), |
| redirect_url); |
| } |
| |
| protected: |
| std::optional<GURL> clicked_url_; |
| std::optional<size_t> clicked_index_; |
| |
| base::OnceClosure blocked_url_added_closure_; |
| raw_ptr<Browser, AcrossTasksDanglingUntriaged> current_browser_; |
| }; |
| |
| // Tests that clicking an item in the list of blocked URLs trigger a navigation |
| // to that URL. |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, ModelAllowsRedirection) { |
| const GURL blocked_urls[] = { |
| embedded_test_server()->GetURL("b.com", "/title1.html"), |
| embedded_test_server()->GetURL("c.com", "/title1.html"), |
| embedded_test_server()->GetURL("d.com", "/title1.html"), |
| }; |
| |
| // Signal that a blocked redirection happened. |
| auto* helper = GetFramebustTabHelper(); |
| for (const GURL& url : blocked_urls) { |
| helper->AddBlockedUrl(url, |
| base::BindOnce(&FramebustBlockBrowserTest::OnClick, |
| base::Unretained(this))); |
| } |
| EXPECT_TRUE(helper->HasBlockedUrls()); |
| |
| // Simulate clicking on the second blocked URL. |
| ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model( |
| browser()->GetFeatures().content_setting_bubble_model_delegate(), |
| GetWebContents()); |
| |
| EXPECT_FALSE(clicked_index_.has_value()); |
| EXPECT_FALSE(clicked_url_.has_value()); |
| |
| content::TestNavigationObserver observer(GetWebContents()); |
| ui::MouseEvent click_event(ui::EventType::kMousePressed, gfx::Point(), |
| gfx::Point(), ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, |
| ui::EF_LEFT_MOUSE_BUTTON); |
| framebust_block_bubble_model.OnListItemClicked(/* index = */ 1, click_event); |
| observer.Wait(); |
| |
| EXPECT_TRUE(clicked_index_.has_value()); |
| EXPECT_TRUE(clicked_url_.has_value()); |
| EXPECT_EQ(1u, clicked_index_.value()); |
| EXPECT_EQ(embedded_test_server()->GetURL("c.com", "/title1.html"), |
| clicked_url_.value()); |
| EXPECT_FALSE(helper->HasBlockedUrls()); |
| EXPECT_EQ(blocked_urls[1], GetWebContents()->GetLastCommittedURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, AllowRadioButtonSelected) { |
| const GURL url = embedded_test_server()->GetURL("/iframe.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| |
| // Signal that a blocked redirection happened. |
| auto* helper = GetFramebustTabHelper(); |
| helper->AddBlockedUrl(url, base::BindOnce(&FramebustBlockBrowserTest::OnClick, |
| base::Unretained(this))); |
| EXPECT_TRUE(helper->HasBlockedUrls()); |
| |
| HostContentSettingsMap* settings_map = |
| HostContentSettingsMapFactory::GetForProfile(browser()->profile()); |
| EXPECT_EQ(CONTENT_SETTING_BLOCK, |
| settings_map->GetContentSetting(url, GURL(), |
| ContentSettingsType::POPUPS)); |
| |
| // Create a content bubble and simulate clicking on the first radio button |
| // before closing it. |
| ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model( |
| browser()->GetFeatures().content_setting_bubble_model_delegate(), |
| GetWebContents()); |
| std::unique_ptr<FakeOwner> owner = FakeOwner::Create( |
| framebust_block_bubble_model, kDisallowRadioButtonIndex); |
| |
| owner->SetSelectedRadioOptionAndCommit(kAllowRadioButtonIndex); |
| |
| EXPECT_EQ(CONTENT_SETTING_ALLOW, |
| settings_map->GetContentSetting(url, GURL(), |
| ContentSettingsType::POPUPS)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, DisallowRadioButtonSelected) { |
| const GURL url = embedded_test_server()->GetURL("/iframe.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| |
| // Signal that a blocked redirection happened. |
| auto* helper = GetFramebustTabHelper(); |
| helper->AddBlockedUrl(url, base::BindOnce(&FramebustBlockBrowserTest::OnClick, |
| base::Unretained(this))); |
| EXPECT_TRUE(helper->HasBlockedUrls()); |
| |
| HostContentSettingsMap* settings_map = |
| HostContentSettingsMapFactory::GetForProfile(browser()->profile()); |
| EXPECT_EQ(CONTENT_SETTING_BLOCK, |
| settings_map->GetContentSetting(url, GURL(), |
| ContentSettingsType::POPUPS)); |
| |
| // Create a content bubble and simulate clicking on the second radio button |
| // before closing it. |
| ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model( |
| browser()->GetFeatures().content_setting_bubble_model_delegate(), |
| GetWebContents()); |
| |
| std::unique_ptr<FakeOwner> owner = |
| FakeOwner::Create(framebust_block_bubble_model, kAllowRadioButtonIndex); |
| |
| owner->SetSelectedRadioOptionAndCommit(kDisallowRadioButtonIndex); |
| |
| EXPECT_EQ(CONTENT_SETTING_BLOCK, |
| settings_map->GetContentSetting(url, GURL(), |
| ContentSettingsType::POPUPS)); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) |
| #define MAYBE_ManageButtonClicked DISABLED_ManageButtonClicked |
| #else |
| #define MAYBE_ManageButtonClicked ManageButtonClicked |
| #endif |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, MAYBE_ManageButtonClicked) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| ash::SystemWebAppManager::GetForTest(browser()->profile()) |
| ->InstallSystemAppsForTesting(); |
| #endif |
| |
| const GURL url = embedded_test_server()->GetURL("/iframe.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| |
| // Signal that a blocked redirection happened. |
| auto* helper = GetFramebustTabHelper(); |
| helper->AddBlockedUrl(url, base::BindOnce(&FramebustBlockBrowserTest::OnClick, |
| base::Unretained(this))); |
| EXPECT_TRUE(helper->HasBlockedUrls()); |
| |
| // Create a content bubble and simulate clicking on the second radio button |
| // before closing it. |
| ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model( |
| browser()->GetFeatures().content_setting_bubble_model_delegate(), |
| GetWebContents()); |
| |
| content::TestNavigationObserver navigation_observer(nullptr); |
| navigation_observer.StartWatchingNewWebContents(); |
| framebust_block_bubble_model.OnManageButtonClicked(); |
| navigation_observer.Wait(); |
| |
| EXPECT_TRUE(base::StartsWith(navigation_observer.last_navigation_url().spec(), |
| chrome::kChromeUISettingsURL, |
| base::CompareCase::SENSITIVE)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, SimpleFramebust_Blocked) { |
| EXPECT_TRUE(ExecuteAndCheckBlockedRedirection()); |
| } |
| |
| // Attempts to navigate to chrome:// URLs should be blocked without allowing the |
| // user to proceed. Instead, the blocked URLs list includes content:kBlockedURL, |
| // which is about:blank#blocked, similar to other cases where a renderer |
| // attempts to navigate to an off-limits URL. See https://crbug.com/375550814. |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, |
| Framebust_WebUI_Blocked_No_Bypass) { |
| const GURL chrome_url(chrome::kChromeUISettingsURL); |
| EXPECT_FALSE(ExecuteAndCheckBlockedRedirection(chrome_url)); |
| EXPECT_TRUE(base::Contains(GetFramebustTabHelper()->blocked_urls(), |
| GURL(content::kBlockedURL))); |
| } |
| |
| // Attempts to navigate to file:// URLs should be blocked without allowing the |
| // user to proceed. Instead, the blocked URLs list includes content:kBlockedURL, |
| // which is about:blank#blocked, similar to other cases where a renderer |
| // attempts to navigate to an off-limits URL. See https://crbug.com/375550814. |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, |
| Framebust_File_Blocked_No_Bypass) { |
| const GURL file_url("file:///"); |
| EXPECT_FALSE(ExecuteAndCheckBlockedRedirection(file_url)); |
| EXPECT_TRUE(base::Contains(GetFramebustTabHelper()->blocked_urls(), |
| GURL(content::kBlockedURL))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, |
| FramebustAllowedByGlobalSetting) { |
| HostContentSettingsMap* settings_map = |
| HostContentSettingsMapFactory::GetForProfile(browser()->profile()); |
| settings_map->SetDefaultContentSetting(ContentSettingsType::POPUPS, |
| CONTENT_SETTING_ALLOW); |
| |
| // Create a new browser to test in to ensure that the render process gets the |
| // updated content settings. |
| CreateAndSetBrowser(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), embedded_test_server()->GetURL("/iframe.html"))); |
| NavigateIframeToUrlWithoutGesture( |
| GetWebContents(), "test", |
| embedded_test_server()->GetURL("a.com", "/title1.html")); |
| |
| content::RenderFrameHost* child = |
| content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0); |
| ASSERT_TRUE(child); |
| |
| GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| |
| content::TestNavigationObserver observer(GetWebContents()); |
| child->ExecuteJavaScriptForTests( |
| base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';", |
| redirect_url.spec().c_str())), |
| base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL); |
| observer.Wait(); |
| EXPECT_TRUE(GetFramebustTabHelper()->blocked_urls().empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, |
| FramebustAllowedBySiteSetting) { |
| GURL top_level_url = embedded_test_server()->GetURL("/iframe.html"); |
| HostContentSettingsMap* settings_map = |
| HostContentSettingsMapFactory::GetForProfile(browser()->profile()); |
| settings_map->SetContentSettingDefaultScope(top_level_url, GURL(), |
| ContentSettingsType::POPUPS, |
| CONTENT_SETTING_ALLOW); |
| |
| // Create a new browser to test in to ensure that the render process gets the |
| // updated content settings. |
| CreateAndSetBrowser(); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), top_level_url)); |
| NavigateIframeToUrlWithoutGesture( |
| GetWebContents(), "test", |
| embedded_test_server()->GetURL("a.com", "/title1.html")); |
| |
| content::RenderFrameHost* child = |
| content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0); |
| ASSERT_TRUE(child); |
| |
| GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| |
| content::TestNavigationObserver observer(GetWebContents()); |
| child->ExecuteJavaScriptForTests( |
| base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';", |
| redirect_url.spec().c_str())), |
| base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL); |
| observer.Wait(); |
| EXPECT_TRUE(GetFramebustTabHelper()->blocked_urls().empty()); |
| } |
| |
| // Regression test for https://crbug.com/894955, where the framebust UI would |
| // persist on subsequent navigations. |
| IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, |
| FramebustBlocked_SubsequentNavigation_NoUI) { |
| EXPECT_TRUE(ExecuteAndCheckBlockedRedirection()); |
| |
| // Now, navigate away and check that the UI went away. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), embedded_test_server()->GetURL("/title2.html"))); |
| |
| // TODO(csharrison): Ideally we could query the actual UI here. For now, just |
| // look at the internal state of the framebust tab helper. |
| EXPECT_FALSE(GetFramebustTabHelper()->HasBlockedUrls()); |
| } |
| |
| class FramebustBlockPrerenderTest : public FramebustBlockBrowserTest { |
| public: |
| FramebustBlockPrerenderTest() |
| : prerender_helper_( |
| base::BindRepeating(&FramebustBlockPrerenderTest::GetWebContents, |
| base::Unretained(this))) {} |
| ~FramebustBlockPrerenderTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| prerender_helper_.RegisterServerRequestMonitor(embedded_test_server()); |
| FramebustBlockBrowserTest::SetUpOnMainThread(); |
| } |
| |
| protected: |
| content::test::PrerenderTestHelper prerender_helper_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockPrerenderTest, |
| FramebustBlocked_PrerenderNavigation) { |
| EXPECT_TRUE(ExecuteAndCheckBlockedRedirection()); |
| |
| // Start a prerender and ensure that the framebust UI persists on the |
| // prerender navigation. |
| const GURL prerender_url = |
| embedded_test_server()->GetURL("/title1.html?prerender"); |
| prerender_helper_.AddPrerender(prerender_url); |
| EXPECT_TRUE(GetFramebustTabHelper()->HasBlockedUrls()); |
| |
| // Activate a prerendered page. |
| prerender_helper_.NavigatePrimaryPage(prerender_url); |
| EXPECT_FALSE(GetFramebustTabHelper()->HasBlockedUrls()); |
| } |
| |
| class FramebustBlockFencedFrameTest : public FramebustBlockBrowserTest { |
| public: |
| FramebustBlockFencedFrameTest() = default; |
| ~FramebustBlockFencedFrameTest() override = default; |
| |
| content::RenderFrameHost* primary_main_frame_host() { |
| return GetWebContents()->GetPrimaryMainFrame(); |
| } |
| |
| protected: |
| content::test::FencedFrameTestHelper fenced_frame_helper_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(FramebustBlockFencedFrameTest, |
| FramebustBlocked_FencedFrameNavigation) { |
| EXPECT_TRUE(ExecuteAndCheckBlockedRedirection()); |
| |
| // Create a fenced frame in the primary main page and ensure that the |
| // framebust UI persists on fenced frame navigation. |
| const GURL fenced_frame_url = |
| embedded_test_server()->GetURL("/fenced_frames/title1.html"); |
| content::RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_helper_.CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url); |
| ASSERT_NE(nullptr, fenced_frame_rfh); |
| |
| EXPECT_TRUE(GetFramebustTabHelper()->HasBlockedUrls()); |
| } |