| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_features.h" |
| #include "chrome/browser/external_protocol/external_protocol_handler.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/shell_integration.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/policy/core/common/policy_pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_context.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/navigation_handle_observer.h" |
| #include "content/public/test/test_navigation_observer.h" |
| |
| class ExternalProtocolHandlerBrowserTest : public InProcessBrowserTest { |
| public: |
| content::WebContents* web_content() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| }; |
| |
| class ExternalProtocolHandlerSandboxBrowserTest |
| : public ExternalProtocolHandlerBrowserTest { |
| public: |
| ExternalProtocolHandlerSandboxBrowserTest() { |
| features_.InitAndEnableFeature(features::kSandboxExternalProtocolBlocked); |
| } |
| |
| void SetUpOnMainThread() override { |
| ExternalProtocolHandlerBrowserTest::SetUpOnMainThread(); |
| AllowCustomProtocol(); |
| } |
| |
| // On Linux, external protocols calls are delegated to the OS without |
| // additional security check. On Windows and Mac (possibly more), it runs |
| // additional security check and ask the user, without displaying a message in |
| // the console. Adopt Linux's behavior for the purpose of this test suite. |
| void AllowCustomProtocol() { |
| base::Value::List allow_list; |
| allow_list.Append("custom:*"); |
| browser()->profile()->GetPrefs()->Set(policy::policy_prefs::kUrlAllowlist, |
| base::Value(std::move(allow_list))); |
| } |
| |
| content::RenderFrameHost* CreateIFrame(content::RenderFrameHost* document, |
| std::string iframe_sandbox) { |
| EXPECT_TRUE(content::ExecJs( |
| document, "const iframe = document.createElement('iframe');" + |
| iframe_sandbox + |
| "iframe.src = '/empty.html';" |
| "document.body.appendChild(iframe)")); |
| |
| EXPECT_TRUE(content::WaitForLoadStop(web_content())); |
| return ChildFrameAt(document, 0); |
| } |
| |
| // Navigate to an external protocol from an iframe or fenced frame. Returns |
| // whether the navigation was allowed by sandbox. `sandbox` is used to define |
| // the sub frame's sandbox flag. |
| bool AllowedBySandbox(content::RenderFrameHost* child_document, |
| bool has_user_gesture = false) { |
| EXPECT_TRUE(child_document); |
| |
| // When not blocked by sandbox, Linux displays |allowed_msg_1| and |
| // Windows/Mac |allowed_msg_2|. That's because Linux do not provide API to |
| // query for the supported protocols before launching the external |
| // application. |
| const char allowed_msg_1[] = |
| "Launched external handler for 'custom:custom'."; |
| const char allowed_msg_2[] = |
| "Failed to launch 'custom:custom' because the scheme does not have a " |
| "registered handler."; |
| const char blocked_msg[] = |
| "Navigation to external protocol blocked by sandbox, because it " |
| "doesn't contain any of: " |
| "'allow-top-navigation-to-custom-protocols', " |
| "'allow-top-navigation-by-user-activation', " |
| "'allow-top-navigation', or " |
| "'allow-popups'. See " |
| "https://chromestatus.com/feature/5680742077038592 and " |
| "https://chromeenterprise.google/policies/" |
| "#SandboxExternalProtocolBlocked"; |
| content::WebContentsConsoleObserver observer(web_content()); |
| observer.SetFilter(base::BindLambdaForTesting( |
| [&](const content::WebContentsConsoleObserver::Message& message) { |
| std::string value = base::UTF16ToUTF8(message.message); |
| return value == allowed_msg_1 || value == allowed_msg_2 || |
| value == blocked_msg; |
| })); |
| |
| EXPECT_TRUE(content::ExecJs( |
| child_document, "location.href = 'custom:custom';", |
| has_user_gesture ? content::EXECUTE_SCRIPT_DEFAULT_OPTIONS |
| : content::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| EXPECT_TRUE(observer.Wait()); |
| std::string message = observer.GetMessageAt(0u); |
| |
| return message == allowed_msg_1 || message == allowed_msg_2; |
| } |
| |
| base::test::ScopedFeatureList features_; |
| }; |
| |
| // Observe that the tab is created then automatically closed. |
| class TabAddedRemovedObserver : public TabStripModelObserver { |
| public: |
| explicit TabAddedRemovedObserver(TabStripModel* tab_strip_model) { |
| tab_strip_model->AddObserver(this); |
| } |
| |
| void OnTabStripModelChanged( |
| TabStripModel* tab_strip_model, |
| const TabStripModelChange& change, |
| const TabStripSelectionChange& selection) override { |
| if (change.type() == TabStripModelChange::kInserted) { |
| inserted_ = true; |
| return; |
| } |
| if (change.type() == TabStripModelChange::kRemoved) { |
| EXPECT_TRUE(inserted_); |
| removed_ = true; |
| loop_.Quit(); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void Wait() { |
| if (inserted_ && removed_) |
| return; |
| loop_.Run(); |
| } |
| |
| private: |
| bool inserted_ = false; |
| bool removed_ = false; |
| base::RunLoop loop_; |
| }; |
| |
| // Flaky on Mac: https://crbug.com/1143762: |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_AutoCloseTabOnNonWebProtocolNavigation DISABLED_AutoCloseTabOnNonWebProtocolNavigation |
| #else |
| #define MAYBE_AutoCloseTabOnNonWebProtocolNavigation AutoCloseTabOnNonWebProtocolNavigation |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerBrowserTest, |
| MAYBE_AutoCloseTabOnNonWebProtocolNavigation) { |
| TabAddedRemovedObserver observer(browser()->tab_strip_model()); |
| ASSERT_EQ(browser()->tab_strip_model()->count(), 1); |
| ASSERT_TRUE( |
| ExecJs(web_content(), "window.open('mailto:test@site.test', '_blank');")); |
| observer.Wait(); |
| EXPECT_EQ(browser()->tab_strip_model()->count(), 1); |
| } |
| |
| // Flaky on Mac: https://crbug.com/1143762: |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ProtocolLaunchEmitsConsoleLog \ |
| DISABLED_ProtocolLaunchEmitsConsoleLog |
| #else |
| #define MAYBE_ProtocolLaunchEmitsConsoleLog ProtocolLaunchEmitsConsoleLog |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerBrowserTest, |
| MAYBE_ProtocolLaunchEmitsConsoleLog) { |
| content::WebContentsConsoleObserver observer(web_content()); |
| // Wait for either "Launched external handler..." or "Failed to launch..."; the former will pass |
| // the test, while the latter will fail it more quickly than waiting for a timeout. |
| observer.SetPattern("*aunch*'mailto:test@site.test'*"); |
| ASSERT_TRUE( |
| ExecJs(web_content(), "window.open('mailto:test@site.test', '_self');")); |
| ASSERT_TRUE(observer.Wait()); |
| ASSERT_EQ(1u, observer.messages().size()); |
| EXPECT_EQ("Launched external handler for 'mailto:test@site.test'.", |
| observer.GetMessageAt(0u)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerBrowserTest, |
| ProtocolFailureEmitsConsoleLog) { |
| // Only on Mac and Windows is there a way for Chromium to know whether a |
| // protocol handler is registered ahead of time. |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) |
| content::WebContentsConsoleObserver observer(web_content()); |
| observer.SetPattern("Failed to launch 'does.not.exist:failure'*"); |
| ASSERT_TRUE( |
| ExecJs(web_content(), "window.open('does.not.exist:failure', '_self');")); |
| ASSERT_TRUE(observer.Wait()); |
| ASSERT_EQ(1u, observer.messages().size()); |
| #endif |
| } |
| |
| // External protocol are allowed when iframe's sandbox contains one of: |
| // - allow-popups |
| // - allow-top-navigation |
| // - allow-top-navigation-by-user-activation + UserGesture |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, Sandbox) { |
| EXPECT_TRUE( |
| AllowedBySandbox(CreateIFrame(web_content()->GetPrimaryMainFrame(), ""))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, SandboxAll) { |
| EXPECT_FALSE( |
| AllowedBySandbox(CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, |
| SandboxAllowPopups) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts allow-popups';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, |
| SandboxAllowTopNavigationToCustomProtocols) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-to-custom-protocols';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, |
| SandboxAllowTopNavigation) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts allow-top-navigation';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, |
| SandboxAllowTopNavigationByUserActivation) { |
| EXPECT_FALSE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-by-user-activation';"), |
| /*user-gesture=*/false)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxBrowserTest, |
| SandboxAllowTopNavigationByUserActivationWithGesture) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-by-user-activation';"), |
| /*user-gesture=*/true)); |
| } |
| |
| namespace { |
| |
| class AlwaysBlockedExternalProtocolHandlerDelegate |
| : public ExternalProtocolHandler::Delegate { |
| public: |
| AlwaysBlockedExternalProtocolHandlerDelegate() { |
| ExternalProtocolHandler::SetDelegateForTesting(this); |
| } |
| ~AlwaysBlockedExternalProtocolHandlerDelegate() override { |
| ExternalProtocolHandler::SetDelegateForTesting(nullptr); |
| } |
| |
| scoped_refptr<shell_integration::DefaultSchemeClientWorker> CreateShellWorker( |
| const GURL& url) override { |
| NOTREACHED(); |
| } |
| ExternalProtocolHandler::BlockState GetBlockState(const std::string& scheme, |
| Profile* profile) override { |
| return ExternalProtocolHandler::BLOCK; |
| } |
| void BlockRequest() override {} |
| void RunExternalProtocolDialog( |
| const GURL& url, |
| content::WebContents* web_contents, |
| ui::PageTransition page_transition, |
| bool has_user_gesture, |
| const std::optional<url::Origin>& initiating_origin, |
| const std::u16string& program_name) override { |
| NOTREACHED(); |
| } |
| void LaunchUrlWithoutSecurityCheck( |
| const GURL& url, |
| content::WebContents* web_contents) override { |
| NOTREACHED(); |
| } |
| void FinishedProcessingCheck() override {} |
| }; |
| |
| } // namespace |
| |
| // Tests (by forcing a particular scheme to be blocked, regardless of platform) |
| // that the console message is attributed to a subframe if one was responsible. |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerBrowserTest, |
| ProtocolLaunchEmitsConsoleLogInCorrectFrame) { |
| AlwaysBlockedExternalProtocolHandlerDelegate always_blocked; |
| content::RenderFrameHost* main_rfh = ui_test_utils::NavigateToURL( |
| browser(), GURL("data:text/html,<iframe srcdoc=\"Hello!\"></iframe>")); |
| ASSERT_TRUE(main_rfh); |
| content::RenderFrameHost* originating_rfh = ChildFrameAt(main_rfh, 0); |
| |
| content::WebContentsConsoleObserver observer( |
| content::WebContents::FromRenderFrameHost(originating_rfh)); |
| observer.SetPattern("*aunch*'willfailtolaunch://foo'*"); |
| observer.SetFilter(base::BindRepeating( |
| [](content::RenderFrameHost* expected_rfh, |
| const content::WebContentsConsoleObserver::Message& message) { |
| return message.source_frame == expected_rfh; |
| }, |
| originating_rfh)); |
| |
| ASSERT_TRUE( |
| ExecJs(originating_rfh, "location.href = 'willfailtolaunch://foo';")); |
| ASSERT_TRUE(observer.Wait()); |
| ASSERT_EQ(1u, observer.messages().size()); |
| EXPECT_EQ("Not allowed to launch 'willfailtolaunch://foo'.", |
| observer.GetMessageAt(0u)); |
| } |
| |
| class ExternalProtocolHandlerSandboxFencedFrameBrowserTest |
| : public ExternalProtocolHandlerSandboxBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| ExternalProtocolHandlerSandboxBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| content::RenderFrameHost* CreateFencedFrame() { |
| return fenced_frame_test_helper().CreateFencedFrame( |
| web_content()->GetPrimaryMainFrame(), |
| embedded_test_server()->GetURL("/fenced_frames/title1.html")); |
| } |
| |
| content::test::FencedFrameTestHelper& fenced_frame_test_helper() { |
| return fenced_frame_test_helper_; |
| } |
| |
| content::test::FencedFrameTestHelper fenced_frame_test_helper_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllWithoutGesture) { |
| EXPECT_TRUE(AllowedBySandbox(CreateFencedFrame(), /*user-gesture=*/false)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllWithGesture) { |
| EXPECT_TRUE(AllowedBySandbox(CreateFencedFrame(), /*user-gesture=*/true)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxInFencedFrame) { |
| EXPECT_TRUE(AllowedBySandbox(CreateIFrame(CreateFencedFrame(), ""))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllInFencedFrame) { |
| EXPECT_FALSE(AllowedBySandbox( |
| CreateIFrame(CreateFencedFrame(), "iframe.sandbox = 'allow-scripts';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllowPopupsInFencedFrame) { |
| EXPECT_TRUE(AllowedBySandbox(CreateIFrame( |
| CreateFencedFrame(), "iframe.sandbox = 'allow-scripts allow-popups';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllowTopNavigationInFencedFrame) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(CreateFencedFrame(), |
| "iframe.sandbox = 'allow-scripts allow-top-navigation';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllowTopNavigationToCustomProtocolsInFencedFrame) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(CreateFencedFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-to-custom-protocols';"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllowTopNavigationByUserActivationInFencedFrame) { |
| EXPECT_FALSE(AllowedBySandbox( |
| CreateIFrame(CreateFencedFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-by-user-activation';"), |
| /*user-gesture=*/false)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| ExternalProtocolHandlerSandboxFencedFrameBrowserTest, |
| SandboxAllowTopNavigationByUserActivationWithGestureInFencedFrame) { |
| EXPECT_TRUE(AllowedBySandbox( |
| CreateIFrame(web_content()->GetPrimaryMainFrame(), |
| "iframe.sandbox = 'allow-scripts " |
| "allow-top-navigation-by-user-activation';"), |
| /*user-gesture=*/true)); |
| } |