blob: f9bc5adeeb4a66cf51c6c902f2bad39a78a9906a [file] [log] [blame]
// 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());
}