blob: aab724c69cd3c4e422ff782a3dda7de3ff9c4715 [file] [log] [blame]
Liam Brady2141dc52024-08-29 23:04:001// Copyright 2024 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/browser/renderer_host/render_frame_host_impl.h"
6#include "content/browser/web_contents/web_contents_impl.h"
7#include "content/public/browser/render_frame_host.h"
8#include "content/public/browser/web_contents.h"
9#include "content/public/test/browser_test.h"
10#include "content/public/test/browser_test_utils.h"
11#include "content/public/test/content_browser_test.h"
12#include "content/public/test/content_browser_test_utils.h"
13#include "content/public/test/test_navigation_observer.h"
14#include "content/shell/browser/shell.h"
15#include "content/test/content_browser_test_utils_internal.h"
16#include "net/dns/mock_host_resolver.h"
17#include "net/test/embedded_test_server/embedded_test_server.h"
18
19namespace content {
20
21class FramebustingBrowserTest : public ContentBrowserTest {
22 public:
23 FramebustingBrowserTest() = default;
24
25 protected:
26 void SetUpOnMainThread() override {
27 host_resolver()->AddRule("*", "127.0.0.1");
28 ASSERT_TRUE(embedded_test_server()->Start());
29 }
30
31 WebContentsImpl* web_contents() const {
32 return static_cast<WebContentsImpl*>(shell()->web_contents());
33 }
34};
35
36// Verifies that cross-origin iframes cannot navigate the top frame to a
37// different origin (sometimes called "framebusting") without user activation.
38//
39// This is non-standard, unspecified behavior.
40// See also https://www.chromestatus.com/features/5851021045661696.
41IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, FailsWithoutUserActivation) {
42 ASSERT_TRUE(NavigateToURL(
43 shell(), embedded_test_server()->GetURL("/defaultresponse")));
44
45 RenderFrameHost* child = CreateSubframe(
46 web_contents(), "child",
47 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
48 /*wait_for_navigation=*/true);
49
50 WebContentsConsoleObserver console_observer(web_contents());
51 console_observer.SetPattern("*permission to navigate the target frame*");
52
53 EXPECT_FALSE(
54 ExecJs(child, "top.location = 'foo'", EXECUTE_SCRIPT_NO_USER_GESTURE));
55
56 ASSERT_TRUE(console_observer.Wait());
57}
58
59// Verifies that cross-origin iframes can navigate the top frame to a different
60// origin (sometimes called "framebusting") with user activation.
61//
62// This is non-standard, unspecified behavior.
63// See also https://www.chromestatus.com/features/5851021045661696.
64IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, SucceedsWithUserActivation) {
65 ASSERT_TRUE(NavigateToURL(
66 shell(), embedded_test_server()->GetURL("/defaultresponse")));
67
68 GURL other_url =
69 embedded_test_server()->GetURL("other.test", "/defaultresponse");
70 RenderFrameHost* child = CreateSubframe(web_contents(), "child", other_url,
71 /*wait_for_navigation=*/true);
72
73 TestNavigationObserver observer(web_contents());
74
75 // By default `ExecJs()` executes the provided script with user activation.
76 EXPECT_TRUE(ExecJs(child, "top.location = '/defaultresponse'"));
77
78 // The top frame is indeed navigated successfully.
79 observer.Wait();
80 EXPECT_EQ(web_contents()->GetLastCommittedURL(), other_url);
81}
82
83// Verifies that cross-origin iframes can navigate the top frame to a different
84// origin (sometimes called "framebusting") with user activation, even after
85// a couple `setTimeout()` calls.
86//
87// This is non-standard, unspecified behavior.
88// See also https://www.chromestatus.com/features/5851021045661696.
89IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
90 SucceedsWithAsyncUserActivation) {
91 ASSERT_TRUE(NavigateToURL(
92 shell(), embedded_test_server()->GetURL("/defaultresponse")));
93
94 GURL other_url =
95 embedded_test_server()->GetURL("other.test", "/defaultresponse");
96 RenderFrameHost* child = CreateSubframe(web_contents(), "child", other_url,
97 /*wait_for_navigation=*/true);
98
99 TestNavigationObserver observer(web_contents());
100
101 // By default `ExecJs()` executes the provided script with a user activation.
102 //
103 // With user activation, the navigation should succeed even through nested
104 // `setTimeout()` calls.
105 EXPECT_TRUE(ExecJs(child, R"(
106 setTimeout(() => {
107 setTimeout(() => {
108 top.location = '/defaultresponse';
109 }, 0);
110 }, 0);
111 )"));
112
113 // The top frame is indeed navigated successfully.
114 observer.Wait();
115 EXPECT_EQ(web_contents()->GetLastCommittedURL(), other_url);
116}
117
118// Verifies that cross-origin unsandboxed iframes cannot escalate the
119// allow-top-navigation sandbox privilege in a child iframe, which would allow
120// it to navigate the top frame to a different origin (sometimes called
121// "framebusting") without user activation.
122//
123// This is non-standard, unspecified behavior.
124// See also https://www.chromestatus.com/features/5851021045661696.
125IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
126 FailsFromGrandchildPrivilegeEscalationInSandboxFlags) {
127 ASSERT_TRUE(NavigateToURL(
128 shell(), embedded_test_server()->GetURL("/defaultresponse")));
129
130 RenderFrameHost* child = CreateSubframe(
131 web_contents()->GetPrimaryMainFrame(), "child",
132 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
133 /*wait_for_navigation=*/true);
134
135 RenderFrameHost* grandchild = CreateSubframe(
136 child, "grandchild",
137 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
138 /*wait_for_navigation=*/true,
139 {.sandbox_flags = "allow-scripts allow-top-navigation"});
140
141 WebContentsConsoleObserver console_observer(web_contents());
142 console_observer.SetPattern("*permission to navigate the target frame*");
143
144 EXPECT_FALSE(ExecJs(grandchild, "window.top.location = 'foo'",
145 EXECUTE_SCRIPT_NO_USER_GESTURE));
146
147 ASSERT_TRUE(console_observer.Wait());
148}
149
150// Verifies that a grandchild cross-origin unsandboxed iframe cannot give itself
151// allow-top-navigation sandbox privileges via its delivered sandbox flags in
152// the HTTP response header, which would allow it to navigate the top frame to a
153// different origin (sometimes called "framebusting") without user activation.
154//
155// This is non-standard, unspecified behavior.
156// See also https://www.chromestatus.com/features/5851021045661696.
157IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
158 FailsFromGrandchildPrivilegeEscalationInDeliveredFlags) {
159 ASSERT_TRUE(NavigateToURL(
160 shell(), embedded_test_server()->GetURL("/defaultresponse")));
161
162 RenderFrameHost* child = CreateSubframe(
163 web_contents()->GetPrimaryMainFrame(), "child",
164 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
165 /*wait_for_navigation=*/true);
166
167 RenderFrameHost* grandchild =
168 CreateSubframe(child, "grandchild",
169 embedded_test_server()->GetURL(
170 "other.test",
171 "/set-header?Content-Security-Policy: sandbox "
172 "allow-scripts allow-top-navigation"),
173 /*wait_for_navigation=*/true);
174
175 WebContentsConsoleObserver console_observer(web_contents());
176 console_observer.SetPattern("*permission to navigate the target frame*");
177
178 EXPECT_FALSE(ExecJs(grandchild, "window.top.location = 'foo'",
179 EXECUTE_SCRIPT_NO_USER_GESTURE));
180
181 ASSERT_TRUE(console_observer.Wait());
182}
183
184// Verifies that a child cross-origin unsandboxed iframe document cannot give
185// itself allow-top-navigation sandbox privileges via its delivered sandbox
186// flags in the HTTP response header, which would allow it to navigate the top
187// frame to a different origin (sometimes called "framebusting") without user
188// activation.
189//
190// This is non-standard, unspecified behavior.
191// See also https://www.chromestatus.com/features/5851021045661696.
192IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
193 FailsFromChildPrivilegeEscalationInDeliveredFlags) {
194 ASSERT_TRUE(NavigateToURL(
195 shell(), embedded_test_server()->GetURL("/defaultresponse")));
196
197 RenderFrameHost* child =
198 CreateSubframe(web_contents()->GetPrimaryMainFrame(), "child",
199 embedded_test_server()->GetURL(
200 "other.test",
201 "/set-header?Content-Security-Policy: sandbox "
202 "allow-scripts allow-top-navigation"),
203 /*wait_for_navigation=*/true);
204
205 WebContentsConsoleObserver console_observer(web_contents());
206 console_observer.SetPattern("*permission to navigate the target frame*");
207
208 EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'",
209 EXECUTE_SCRIPT_NO_USER_GESTURE));
210
211 ASSERT_TRUE(console_observer.Wait());
212}
213
214// Verifies that a navigation to a cross-site document consumes sticky user
215// activation, preventing the new document from navigating the top frame to a
216// different origin (sometimes called "framebusting") without user activation.
217//
218// This is non-standard, unspecified behavior.
219// See also https://www.chromestatus.com/features/5851021045661696.
220IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest, FailsAfterCrossSiteNavigation) {
221 ASSERT_TRUE(NavigateToURL(
222 shell(), embedded_test_server()->GetURL("/defaultresponse")));
223
224 RenderFrameHost* child = CreateSubframe(
225 web_contents()->GetPrimaryMainFrame(), "child",
226 embedded_test_server()->GetURL("foo.com", "/defaultresponse"),
227 /*wait_for_navigation=*/true);
228
229 // Give the child iframe user activation.
230 EXPECT_TRUE(ExecJs(child, ""));
231
232 // Perform a cross-site navigation. This should clear the sticky user
233 // activation state.
234 GURL navigate_url =
235 embedded_test_server()->GetURL("other.test", "/defaultresponse");
236 EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url),
237 EXECUTE_SCRIPT_NO_USER_GESTURE));
238 EXPECT_TRUE(content::WaitForLoadStop(web_contents()));
239 child =
240 web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host();
241 EXPECT_EQ(child->GetLastCommittedURL(), navigate_url);
242
243 WebContentsConsoleObserver console_observer(web_contents());
244 console_observer.SetPattern("*permission to navigate the target frame*");
245
246 EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'",
247 EXECUTE_SCRIPT_NO_USER_GESTURE));
248
249 ASSERT_TRUE(console_observer.Wait());
250}
251
252// Verifies that a navigation to a same-site document maintains sticky user
253// activation, allow the new document to navigate the top frame to a
254// different origin (sometimes called "framebusting") without transient user
255// activation.
256//
257// This is non-standard, unspecified behavior.
258// See also https://www.chromestatus.com/features/5851021045661696.
259IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
260 SucceedsAfterSameSiteNavigation) {
261 ASSERT_TRUE(NavigateToURL(
262 shell(), embedded_test_server()->GetURL("/defaultresponse")));
263
264 RenderFrameHost* child = CreateSubframe(
265 web_contents()->GetPrimaryMainFrame(), "child",
266 embedded_test_server()->GetURL("foo.com", "/defaultresponse"),
267 /*wait_for_navigation=*/true);
268
269 // Give the child iframe user activation.
270 EXPECT_TRUE(ExecJs(child, ""));
271
272 // Perform a same-site but cross-origin navigation. This should keep the
273 // sticky user activation state.
274 GURL navigate_url =
275 embedded_test_server()->GetURL("subdomain.foo.com", "/defaultresponse");
276 EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url),
277 EXECUTE_SCRIPT_NO_USER_GESTURE));
278 EXPECT_TRUE(content::WaitForLoadStop(web_contents()));
279 child =
280 web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host();
281 EXPECT_EQ(child->GetLastCommittedURL(), navigate_url);
282
283 EXPECT_TRUE(ExecJs(child, "window.top.location = 'foo'",
284 EXECUTE_SCRIPT_NO_USER_GESTURE));
285}
286
287// Verifies that a navigation to a same-site document without sticky user
288// activation keeps the unset activation state, preventing the new document from
289// navigating the top frame to a different origin (sometimes called
290// "framebusting") without transient user activation.
291//
292// This is non-standard, unspecified behavior.
293// See also https://www.chromestatus.com/features/5851021045661696.
294IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
295 FailsAfterSameSiteNavigationWithoutUserActivation) {
296 ASSERT_TRUE(NavigateToURL(
297 shell(), embedded_test_server()->GetURL("/defaultresponse")));
298
299 RenderFrameHost* child = CreateSubframe(
300 web_contents()->GetPrimaryMainFrame(), "child",
301 embedded_test_server()->GetURL("foo.com", "/defaultresponse"),
302 /*wait_for_navigation=*/true);
303
304 // Perform a same-site but cross-origin navigation. There is no sticky user
305 // activation state, so the newly navigated page should not have sticky user
306 // activation either.
307 GURL navigate_url =
308 embedded_test_server()->GetURL("subdomain.foo.com", "/defaultresponse");
309 EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1", navigate_url),
310 EXECUTE_SCRIPT_NO_USER_GESTURE));
311 EXPECT_TRUE(content::WaitForLoadStop(web_contents()));
312 child =
313 web_contents()->GetPrimaryMainFrame()->child_at(0)->current_frame_host();
314 EXPECT_EQ(child->GetLastCommittedURL(), navigate_url);
315
316 WebContentsConsoleObserver console_observer(web_contents());
317 console_observer.SetPattern("*permission to navigate the target frame*");
318
319 EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'",
320 EXECUTE_SCRIPT_NO_USER_GESTURE));
321
322 ASSERT_TRUE(console_observer.Wait());
323}
324
325// Verifies that cross-origin iframes sandboxed with
326// "allow-top-navigation-by-user-activation" can only navigate the top frame to
327// a different origin (sometimes called "framebusting") when they have user
328// activation.
329IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
330 AllowTopNavigationByUserActivation) {
331 ASSERT_TRUE(NavigateToURL(
332 shell(), embedded_test_server()->GetURL("/defaultresponse")));
333
334 RenderFrameHost* child = CreateSubframe(
335 web_contents()->GetPrimaryMainFrame(), "child",
336 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
337 /*wait_for_navigation=*/true,
338 {.sandbox_flags =
339 "allow-scripts allow-top-navigation-by-user-activation"});
340
341 WebContentsConsoleObserver console_observer(web_contents());
342 console_observer.SetPattern("*permission to navigate the target frame*");
343
344 // The initial top-level navigation should fail without user activation.
345 EXPECT_FALSE(ExecJs(child, "window.top.location = 'foo'",
346 EXECUTE_SCRIPT_NO_USER_GESTURE));
347
348 ASSERT_TRUE(console_observer.Wait());
349
350 // Once the frame has user activation, the top-level navigation should
351 // succeed.
352 EXPECT_TRUE(ExecJs(child, ""));
353 EXPECT_TRUE(ExecJs(child, "window.top.location = 'foo'",
354 EXECUTE_SCRIPT_NO_USER_GESTURE));
355}
356
357// Verifies that cross-origin iframes can navigate the top frame to another URL
358// belonging to the top frame's origin without user activation.
359//
360// This is non-standard, unspecified behavior.
361// See also https://www.chromestatus.com/features/5851021045661696.
362IN_PROC_BROWSER_TEST_F(FramebustingBrowserTest,
363 SucceedsInSameOriginWithoutUserActivation) {
364 ASSERT_TRUE(NavigateToURL(
365 shell(), embedded_test_server()->GetURL("/defaultresponse")));
366
367 RenderFrameHost* child = CreateSubframe(
368 web_contents(), "child",
369 embedded_test_server()->GetURL("other.test", "/defaultresponse"),
370 /*wait_for_navigation=*/true);
371
372 TestNavigationObserver observer(web_contents());
373
374 GURL destination = embedded_test_server()->GetURL("/echo");
375 EXPECT_TRUE(ExecJs(child, JsReplace("top.location = $1", destination),
376 EXECUTE_SCRIPT_NO_USER_GESTURE));
377
378 // The top frame is indeed navigated successfully.
379 observer.Wait();
380 EXPECT_EQ(web_contents()->GetLastCommittedURL(), destination);
381}
382
383} // namespace content