0
Files
src/content/browser/framebusting_browsertest.cc
Liam Brady 2141dc586e Refactor some sandbox top-navigation tests as browsertests.
A series of tentative WPTs were written to test updates to our
framebusting interventions. Some of the tests written test non-standard
behavior, and have been resulting in failures when other browsers (who
have not implemented our framebusting interventions) try to run them.
There are also issues with some of the tests themselves when running in
headless mode, as they require giving user activation to a subframe in a
separate window the test opens, which is something headless mode doesn't
currently support. To fix both of these problems, this CL refactors the
problematic tests into browser tests.

Most of the WPTs are staying as is, since they test standard behavior
and don't have compatibility issues with headless mode.

The following tests are being refactored into browser tests:
* sandbox-top-navigation-cross-origin-escalate
  * now FramebustingFromPrivilegeEscalationFails
* sandbox-top-navigation-child-cross-origin-delivered
  * now FramebustingFromDeliveredFlagsFails
* sandbox-top-navigation-cross-site
  * now FramebustingAfterCrossSiteNavigationFails
* sandbox-top-navigation-grandchild-sandboxed-escalate
  * now FramebustingFromGrandchildPrivilegeEscalationFails
* sandbox-top-navigation-same-site-no-activation
  * now FramebustingAfterSameSiteNavigationWithoutUserActivationFails
* sandbox-top-navigation-same-site
  * now FramebustingAfterSameSiteNavigationSucceeds

The following tests have duplicates and are being removed altogether:
* sandbox-top-navigation-child-frame-both
  * duplicate of iframe_sandbox_allow_top_navigation-3
* sandbox-top-navigation-child-frame
  * duplicate of iframe_sandbox_allow_top_navigation-1
* sandbox-top-navigation-grandchild-unsandboxed
  * duplicate of sandbox-top-navigation-grandchild-unsandboxed-cross-origin-parent
* sandbox-top-navigation-user-activation-no-sticky
  * duplicate of iframe_sandbox_allow_top_navigation_by_user_activation_without_user_gesture
* sandbox-top-navigation-user-activation-sticky
  * duplicate of
    iframe_sandbox_allow_top_navigation_by_user_activation-manual

The following tests rely on manual behavior and, while not being
removed, are getting an equivalent browser test:
* iframe_sandbox_allow_top_navigation_by_user_activation_without_user_gesture
  * now FramebustingWithAllowTopNavigationByUserActivation
* iframe_sandbox_allow_top_navigation_by_user_activation-manual
  * also now FramebustingWithAllowTopNavigationByUserActivation

Bug: 347782854
Change-Id: I3b39a9d51db3dd725c8a88eac0bb464fa4753c04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5813532
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Commit-Queue: Liam Brady <lbrady@google.com>
Cr-Commit-Position: refs/heads/main@{#1348945}
2024-08-29 23:04:00 +00:00

384 lines
16 KiB
C++

// 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