| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| |
| namespace content { |
| |
| class FramebustingBrowserTest : public ContentBrowserTest { |
| public: |
| FramebustingBrowserTest() = default; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| WebContentsImpl* web_contents() const { |
| return static_cast<WebContentsImpl*>(shell()->web_contents()); |
| } |
| }; |
| |
| // Verifies that cross-origin iframes cannot navigate the top frame to a |
| // different origin (sometimes called "framebusting") without user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, FailsWithoutUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents(), "child", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE( |
| ExecJs(child, "top.location = 'foo'", EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that cross-origin iframes can navigate the top frame to a different |
| // origin (sometimes called "framebusting") with user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, SucceedsWithUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| GURL other_url = |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"); |
| RenderFrameHost* child = CreateSubframe(web_contents(), "child", other_url, |
| /*wait_for_navigation=*/true); |
| |
| TestNavigationObserver observer(web_contents()); |
| |
| // By default `ExecJs()` executes the provided script with user activation. |
| EXPECT_TRUE(ExecJs(child, "top.location = '/defaultresponse'")); |
| |
| // The top frame is indeed navigated successfully. |
| observer.Wait(); |
| EXPECT_EQ(web_contents()->GetLastCommittedURL(), other_url); |
| } |
| |
| // Verifies that cross-origin iframes can navigate the top frame to a different |
| // origin (sometimes called "framebusting") with user activation, even after |
| // a couple `setTimeout()` calls. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| SucceedsWithAsyncUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| GURL other_url = |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"); |
| RenderFrameHost* child = CreateSubframe(web_contents(), "child", other_url, |
| /*wait_for_navigation=*/true); |
| |
| TestNavigationObserver observer(web_contents()); |
| |
| // By default `ExecJs()` executes the provided script with a user activation. |
| // |
| // With user activation, the navigation should succeed even through nested |
| // `setTimeout()` calls. |
| EXPECT_TRUE(ExecJs(child, R"( |
| setTimeout(() => { |
| setTimeout(() => { |
| top.location = '/defaultresponse'; |
| }, 0); |
| }, 0); |
| )")); |
| |
| // The top frame is indeed navigated successfully. |
| observer.Wait(); |
| EXPECT_EQ(web_contents()->GetLastCommittedURL(), other_url); |
| } |
| |
| // Verifies that cross-origin unsandboxed iframes cannot escalate the |
| // allow-top-navigation sandbox privilege in a child iframe, which would allow |
| // it to navigate the top frame to a different origin (sometimes called |
| // "framebusting") without user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| FailsFromGrandchildPrivilegeEscalationInSandboxFlags) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| RenderFrameHost* grandchild = CreateSubframe( |
| child, "grandchild", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true, |
| {.sandbox_flags = "allow-scripts allow-top-navigation"}); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE(ExecJs(grandchild, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that a grandchild cross-origin unsandboxed iframe cannot give itself |
| // allow-top-navigation sandbox privileges via its delivered sandbox flags in |
| // the HTTP response header, which would allow it to navigate the top frame to a |
| // different origin (sometimes called "framebusting") without user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| FailsFromGrandchildPrivilegeEscalationInDeliveredFlags) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| RenderFrameHost* grandchild = |
| CreateSubframe(child, "grandchild", |
| embedded_test_server()->GetURL( |
| "other.test", |
| "/set-header?Content-Security-Policy: sandbox " |
| "allow-scripts allow-top-navigation"), |
| /*wait_for_navigation=*/true); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE(ExecJs(grandchild, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that a child cross-origin unsandboxed iframe document cannot give |
| // itself allow-top-navigation sandbox privileges via its delivered sandbox |
| // flags in the HTTP response header, which would allow it to navigate the top |
| // frame to a different origin (sometimes called "framebusting") without user |
| // activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| FailsFromChildPrivilegeEscalationInDeliveredFlags) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = |
| CreateSubframe(web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL( |
| "other.test", |
| "/set-header?Content-Security-Policy: sandbox " |
| "allow-scripts allow-top-navigation"), |
| /*wait_for_navigation=*/true); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that a navigation to a cross-site document consumes sticky user |
| // activation, preventing the new document from navigating the top frame to a |
| // different origin (sometimes called "framebusting") without user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, FailsAfterCrossSiteNavigation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("foo.com", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| // Give the child iframe user activation. |
| EXPECT_TRUE(ExecJs(child, "")); |
| |
| // Perform a cross-site navigation. This should clear the sticky user |
| // activation state. |
| GURL navigate_url = |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"); |
| EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents())); |
| child = |
| web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host(); |
| EXPECT_EQ(child->GetLastCommittedURL(), navigate_url); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that a navigation to a same-site document maintains sticky user |
| // activation, allow the new document to navigate the top frame to a |
| // different origin (sometimes called "framebusting") without transient user |
| // activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| SucceedsAfterSameSiteNavigation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("foo.com", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| // Give the child iframe user activation. |
| EXPECT_TRUE(ExecJs(child, "")); |
| |
| // Perform a same-site but cross-origin navigation. This should keep the |
| // sticky user activation state. |
| GURL navigate_url = |
| embedded_test_server()->GetURL("subdomain.foo.com", "/defaultresponse"); |
| EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents())); |
| child = |
| web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host(); |
| EXPECT_EQ(child->GetLastCommittedURL(), navigate_url); |
| |
| EXPECT_TRUE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| } |
| |
| // Verifies that a navigation to a same-site document without sticky user |
| // activation keeps the unset activation state, preventing the new document from |
| // navigating the top frame to a different origin (sometimes called |
| // "framebusting") without transient user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| FailsAfterSameSiteNavigationWithoutUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("foo.com", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| // Perform a same-site but cross-origin navigation. There is no sticky user |
| // activation state, so the newly navigated page should not have sticky user |
| // activation either. |
| GURL navigate_url = |
| embedded_test_server()->GetURL("subdomain.foo.com", "/defaultresponse"); |
| EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents())); |
| child = |
| web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host(); |
| EXPECT_EQ(child->GetLastCommittedURL(), navigate_url); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Verifies that cross-origin iframes sandboxed with |
| // "allow-top-navigation-by-user-activation" can only navigate the top frame to |
| // a different origin (sometimes called "framebusting") when they have user |
| // activation. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| AllowTopNavigationByUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents()->GetPrimaryMainFrame(), "child", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true, |
| {.sandbox_flags = |
| "allow-scripts allow-top-navigation-by-user-activation"}); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("*permission to navigate the target frame*"); |
| |
| // The initial top-level navigation should fail without user activation. |
| EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| |
| // Once the frame has user activation, the top-level navigation should |
| // succeed. |
| EXPECT_TRUE(ExecJs(child, "")); |
| EXPECT_TRUE(ExecJs(child, "window.top.location = 'foo'", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| } |
| |
| // Verifies that cross-origin iframes can navigate the top frame to another URL |
| // belonging to the top frame's origin without user activation. |
| // |
| // This is non-standard, unspecified behavior. |
| // See also https://www.chromestatus.com/features/5851021045661696. |
| IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, |
| SucceedsInSameOriginWithoutUserActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/defaultresponse"))); |
| |
| RenderFrameHost* child = CreateSubframe( |
| web_contents(), "child", |
| embedded_test_server()->GetURL("other.test", "/defaultresponse"), |
| /*wait_for_navigation=*/true); |
| |
| TestNavigationObserver observer(web_contents()); |
| |
| GURL destination = embedded_test_server()->GetURL("/echo"); |
| EXPECT_TRUE(ExecJs(child, JsReplace("top.location = $1", destination), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| // The top frame is indeed navigated successfully. |
| observer.Wait(); |
| EXPECT_EQ(web_contents()->GetLastCommittedURL(), destination); |
| } |
| |
| } // namespace content |