blob: aab724c69cd3c4e422ff782a3dda7de3ff9c4715 [file] [log] [blame]
// 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