1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
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 <memory>
6
7 #include "base/command_line.h"
8 #include "base/files/file_path.h"
9 #include "base/files/file_util.h"
10 #include "base/files/scoped_temp_dir.h"
11 #include "base/macros.h"
12 #include "base/run_loop.h"
13 #include "base/strings/stringprintf.h"
14 #include "content/browser/child_process_security_policy_impl.h"
15 #include "content/browser/frame_host/frame_tree_node.h"
16 #include "content/browser/frame_host/render_frame_host_impl.h"
17 #include "content/browser/web_contents/web_contents_impl.h"
18 #include "content/common/frame_messages.h"
19 #include "content/public/browser/navigation_entry.h"
20 #include "content/public/browser/navigation_handle.h"
21 #include "content/public/browser/render_frame_host.h"
22 #include "content/public/browser/render_process_host.h"
23 #include "content/public/browser/web_contents.h"
24 #include "content/public/common/content_switches.h"
25 #include "content/public/test/browser_test_utils.h"
26 #include "content/public/test/content_browser_test.h"
27 #include "content/public/test/content_browser_test_utils.h"
28 #include "content/public/test/navigation_handle_observer.h"
29 #include "content/public/test/test_navigation_observer.h"
30 #include "content/shell/browser/shell.h"
31 #include "content/shell/browser/shell_content_browser_client.h"
32 #include "content/shell/common/shell_switches.h"
33 #include "content/test/content_browser_test_utils_internal.h"
34 #include "net/base/escape.h"
35 #include "net/dns/mock_host_resolver.h"
36 #include "net/test/embedded_test_server/embedded_test_server.h"
37 #include "net/url_request/url_request.h"
38 #include "net/url_request/url_request_status.h"
39 #include "testing/gmock/include/gmock/gmock-matchers.h"
40 #include "url/gurl.h"
41
42 namespace content {
43
44 // WebContentsDelegate that fails to open a URL when there's a request that
45 // needs to be transferred between renderers.
46 class NoTransferRequestDelegate : public WebContentsDelegate {
47 public:
NoTransferRequestDelegate()48 NoTransferRequestDelegate() {}
49
ShouldTransferNavigation(bool is_main_frame_navigation)50 bool ShouldTransferNavigation(bool is_main_frame_navigation) override {
51 // Intentionally cancel the transfer.
52 return false;
53 }
54
55 private:
56 DISALLOW_COPY_AND_ASSIGN(NoTransferRequestDelegate);
57 };
58
59 class CrossSiteTransferTest : public ContentBrowserTest {
60 public:
CrossSiteTransferTest()61 CrossSiteTransferTest() {}
62
63 // ContentBrowserTest implementation:
SetUpOnMainThread()64 void SetUpOnMainThread() override {
65 host_resolver()->AddRule("*", "127.0.0.1");
66 content::SetupCrossSiteRedirector(embedded_test_server());
67 ASSERT_TRUE(embedded_test_server()->Start());
68 }
69
70 protected:
NavigateToURLContentInitiated(Shell * window,const GURL & url,bool should_replace_current_entry,bool should_wait_for_navigation)71 void NavigateToURLContentInitiated(Shell* window,
72 const GURL& url,
73 bool should_replace_current_entry,
74 bool should_wait_for_navigation) {
75 std::unique_ptr<TestNavigationManager> navigation_manager =
76 should_wait_for_navigation
77 ? std::unique_ptr<TestNavigationManager>(
78 new TestNavigationManager(window->web_contents(), url))
79 : nullptr;
80 std::string script;
81 if (should_replace_current_entry)
82 script = base::StringPrintf("location.replace('%s')", url.spec().c_str());
83 else
84 script = base::StringPrintf("location.href = '%s'", url.spec().c_str());
85 bool result = ExecuteScript(window, script);
86 EXPECT_TRUE(result);
87 if (should_wait_for_navigation) {
88 EXPECT_TRUE(navigation_manager->WaitForRequestStart());
89 EXPECT_TRUE(navigation_manager->WaitForResponse());
90 navigation_manager->WaitForNavigationFinished();
91 EXPECT_TRUE(navigation_manager->was_successful());
92 }
93 }
94
SetUpCommandLine(base::CommandLine * command_line)95 void SetUpCommandLine(base::CommandLine* command_line) override {
96 IsolateAllSitesForTesting(command_line);
97 }
98 };
99
100 // The following tests crash in the ThreadSanitizer runtime,
101 // http://crbug.com/356758.
102 #if defined(THREAD_SANITIZER)
103 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
104 DISABLED_ReplaceEntryCrossProcessThenTransfer
105 #define MAYBE_ReplaceEntryCrossProcessTwice \
106 DISABLED_ReplaceEntryCrossProcessTwice
107 #else
108 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
109 ReplaceEntryCrossProcessThenTransfer
110 #define MAYBE_ReplaceEntryCrossProcessTwice ReplaceEntryCrossProcessTwice
111 #endif
112 // Tests that the |should_replace_current_entry| flag persists correctly across
113 // request transfers that began with a cross-process navigation.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,MAYBE_ReplaceEntryCrossProcessThenTransfer)114 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
115 MAYBE_ReplaceEntryCrossProcessThenTransfer) {
116 NavigationController& controller = shell()->web_contents()->GetController();
117
118 // Navigate to a starting URL, so there is a history entry to replace.
119 GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
120 EXPECT_TRUE(NavigateToURL(shell(), url1));
121
122 // Navigate to a page on A.com with entry replacement. This navigation is
123 // cross-site, so the renderer will send it to the browser via OpenURL to give
124 // to a new process. It will then be transferred into yet another process due
125 // to the call above.
126 GURL url2 =
127 embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
128 NavigateToURLContentInitiated(shell(), url2, true, true);
129
130 // There should be one history entry. url2 should have replaced url1.
131 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
132 EXPECT_EQ(1, controller.GetEntryCount());
133 EXPECT_EQ(0, controller.GetCurrentEntryIndex());
134 EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
135
136 // Now navigate as before to a page on B.com, but normally (without
137 // replacement). This will still perform a double process-swap as above, via
138 // OpenURL and then transfer.
139 GURL url3 =
140 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
141 NavigateToURLContentInitiated(shell(), url3, false, true);
142
143 // There should be two history entries. url2 should have replaced url1. url2
144 // should not have replaced url3.
145 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
146 EXPECT_EQ(2, controller.GetEntryCount());
147 EXPECT_EQ(1, controller.GetCurrentEntryIndex());
148 EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
149 EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
150 }
151
152 // Tests that the |should_replace_current_entry| flag persists correctly across
153 // request transfers that began with a content-initiated in-process
154 // navigation. This test is the same as the test above, except transfering from
155 // in-process.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,ReplaceEntryInProcessThenTransfer)156 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
157 ReplaceEntryInProcessThenTransfer) {
158 NavigationController& controller = shell()->web_contents()->GetController();
159
160 // Navigate to a starting URL, so there is a history entry to replace.
161 GURL url = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
162 EXPECT_TRUE(NavigateToURL(shell(), url));
163
164 // Navigate in-process with entry replacement. It will then be transferred
165 // into a new one due to the call above.
166 GURL url2 = embedded_test_server()->GetURL("/site_isolation/blank.html?2");
167 NavigateToURLContentInitiated(shell(), url2, true, true);
168
169 // There should be one history entry. url2 should have replaced url1.
170 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
171 EXPECT_EQ(1, controller.GetEntryCount());
172 EXPECT_EQ(0, controller.GetCurrentEntryIndex());
173 EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
174
175 // Now navigate as before, but without replacement.
176 GURL url3 = embedded_test_server()->GetURL("/site_isolation/blank.html?3");
177 NavigateToURLContentInitiated(shell(), url3, false, true);
178
179 // There should be two history entries. url2 should have replaced url1. url2
180 // should not have replaced url3.
181 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
182 EXPECT_EQ(2, controller.GetEntryCount());
183 EXPECT_EQ(1, controller.GetCurrentEntryIndex());
184 EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
185 EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
186 }
187
188 // Tests that the |should_replace_current_entry| flag persists correctly across
189 // request transfers that cross processes twice from renderer policy.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,MAYBE_ReplaceEntryCrossProcessTwice)190 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
191 MAYBE_ReplaceEntryCrossProcessTwice) {
192 NavigationController& controller = shell()->web_contents()->GetController();
193
194 // Navigate to a starting URL, so there is a history entry to replace.
195 GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
196 EXPECT_TRUE(NavigateToURL(shell(), url1));
197
198 // Navigate to a page on A.com which redirects to B.com with entry
199 // replacement. This will switch processes via OpenURL twice. First to A.com,
200 // and second in response to the server redirect to B.com. The second swap is
201 // also renderer-initiated via OpenURL because decidePolicyForNavigation is
202 // currently applied on redirects.
203 GURL::Replacements replace_host;
204 GURL url2b =
205 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?2");
206 GURL url2a = embedded_test_server()->GetURL(
207 "A.com", "/cross-site/" + url2b.host() + url2b.PathForRequest());
208 NavigateToURLContentInitiated(shell(), url2a, true, true);
209
210 // There should be one history entry. url2b should have replaced url1.
211 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
212 EXPECT_EQ(1, controller.GetEntryCount());
213 EXPECT_EQ(0, controller.GetCurrentEntryIndex());
214 EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
215
216 // Now repeat without replacement.
217 GURL url3b =
218 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
219 GURL url3a = embedded_test_server()->GetURL(
220 "A.com", "/cross-site/" + url3b.host() + url3b.PathForRequest());
221 NavigateToURLContentInitiated(shell(), url3a, false, true);
222
223 // There should be two history entries. url2b should have replaced url1. url3b
224 // should not have replaced url2b.
225 EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
226 EXPECT_EQ(2, controller.GetEntryCount());
227 EXPECT_EQ(1, controller.GetCurrentEntryIndex());
228 EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
229 EXPECT_EQ(url3b, controller.GetEntryAtIndex(1)->GetURL());
230 }
231
232 // Tests that the request is destroyed when a cross process navigation is
233 // cancelled.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,NoLeakOnCrossSiteCancel)234 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoLeakOnCrossSiteCancel) {
235 NavigationController& controller = shell()->web_contents()->GetController();
236
237 // Navigate to a starting URL, so there is a history entry to replace.
238 GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
239 EXPECT_TRUE(NavigateToURL(shell(), url1));
240
241 NoTransferRequestDelegate no_transfer_request_delegate;
242 WebContentsDelegate* old_delegate = shell()->web_contents()->GetDelegate();
243 shell()->web_contents()->SetDelegate(&no_transfer_request_delegate);
244
245 // Navigate to a page on A.com with entry replacement. This navigation is
246 // cross-site, so the renderer will send it to the browser via OpenURL to give
247 // to a new process. It will then be transferred into yet another process due
248 // to the call above.
249 GURL url2 =
250 embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
251 TestNavigationManager navigation_manager(shell()->web_contents(), url2);
252
253 NavigationHandleObserver handle_observer(shell()->web_contents(), url2);
254 // Don't wait for the navigation to complete, since that never happens in
255 // this case.
256 NavigateToURLContentInitiated(shell(), url2, false, false);
257
258 // Make sure the request for url2 did not complete.
259 EXPECT_FALSE(navigation_manager.WaitForResponse());
260
261 // There should be one history entry, with url1.
262 EXPECT_EQ(1, controller.GetEntryCount());
263 EXPECT_EQ(0, controller.GetCurrentEntryIndex());
264 EXPECT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());
265
266 EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code());
267 shell()->web_contents()->SetDelegate(old_delegate);
268 }
269
270 // Test that verifies that a cross-process transfer retains ability to read
271 // files encapsulated by HTTP POST body that is forwarded to the new renderer.
272 // Invalid handling of this scenario has been suspected as the cause of at least
273 // some of the renderer kills tracked in https://crbug.com/613260.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,PostWithFileData)274 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, PostWithFileData) {
275 // Navigate to the page with form that posts via 307 redirection to
276 // |redirect_target_url| (cross-site from |form_url|). Using 307 (rather than
277 // 302) redirection is important to preserve the HTTP method and POST body.
278 GURL form_url(embedded_test_server()->GetURL(
279 "a.com", "/form_that_posts_cross_site.html"));
280 GURL redirect_target_url(embedded_test_server()->GetURL("x.com", "/echoall"));
281 EXPECT_TRUE(NavigateToURL(shell(), form_url));
282
283 // Prepare a file to upload.
284 base::ScopedAllowBlockingForTesting allow_blocking;
285 base::ScopedTempDir temp_dir;
286 base::FilePath file_path;
287 std::string file_content("test-file-content");
288 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
289 ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
290 ASSERT_LT(
291 0, base::WriteFile(file_path, file_content.data(), file_content.size()));
292
293 base::RunLoop run_loop;
294 // Fill out the form to refer to the test file.
295 std::unique_ptr<FileChooserDelegate> delegate(
296 new FileChooserDelegate(file_path, run_loop.QuitClosure()));
297 shell()->web_contents()->SetDelegate(delegate.get());
298 EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
299 "document.getElementById('file').click();"));
300 run_loop.Run();
301
302 // Remember the old process id for a sanity check below.
303 int old_process_id =
304 shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();
305
306 // Submit the form.
307 TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
308 EXPECT_TRUE(
309 ExecuteScript(shell(), "document.getElementById('file-form').submit();"));
310 form_post_observer.Wait();
311
312 // Verify that we arrived at the expected, redirected location.
313 EXPECT_EQ(redirect_target_url,
314 shell()->web_contents()->GetLastCommittedURL());
315
316 // Verify that the test really verifies access of a *new* renderer process.
317 int new_process_id =
318 shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();
319 ASSERT_NE(new_process_id, old_process_id);
320
321 // MAIN VERIFICATION: Check if the new renderer process is able to read the
322 // file.
323 EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
324 new_process_id, file_path));
325
326 // Verify that POST body got preserved by 307 redirect. This expectation
327 // comes from: https://tools.ietf.org/html/rfc7231#section-6.4.7
328 std::string actual_page_body;
329 EXPECT_TRUE(ExecuteScriptAndExtractString(
330 shell()->web_contents(),
331 "window.domAutomationController.send("
332 "document.getElementsByTagName('pre')[0].innerText);",
333 &actual_page_body));
334 EXPECT_THAT(actual_page_body, ::testing::HasSubstr(file_content));
335 EXPECT_THAT(actual_page_body,
336 ::testing::HasSubstr(file_path.BaseName().AsUTF8Unsafe()));
337 EXPECT_THAT(actual_page_body,
338 ::testing::HasSubstr("form-data; name=\"file\""));
339 }
340
341 // Test that verifies that if navigation originator doesn't have access to a
342 // file, then no access is granted after a cross-process transfer of POST data.
343 // This is a regression test for https://crbug.com/726067.
344 //
345 // This test is somewhat similar to
346 // http/tests/navigation/form-targets-cross-site-frame-post.html web test
347 // except that it 1) tests with files, 2) simulates a malicious scenario and 3)
348 // verifies file access (all of these 3 things are not possible with web
349 // tests).
350 //
351 // This test is very similar to CrossSiteTransferTest.PostWithFileData above,
352 // except that it simulates a malicious form / POST originator.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,MaliciousPostWithFileData)353 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, MaliciousPostWithFileData) {
354 // The initial test window is a named form target.
355 GURL initial_target_url(
356 embedded_test_server()->GetURL("initial-target.com", "/title1.html"));
357 EXPECT_TRUE(NavigateToURL(shell(), initial_target_url));
358 WebContents* target_contents = shell()->web_contents();
359 EXPECT_TRUE(ExecuteScript(target_contents, "window.name = 'form-target';"));
360
361 // Create a new window containing a form targeting |target_contents|.
362 GURL form_url(embedded_test_server()->GetURL(
363 "main.com", "/form_that_posts_cross_site.html"));
364 Shell* other_window = OpenPopup(target_contents, form_url, "form-window");
365 WebContents* form_contents = other_window->web_contents();
366 EXPECT_TRUE(ExecuteScript(
367 form_contents,
368 "document.getElementById('file-form').target = 'form-target';"));
369
370 // Verify the current locations and process placement of |target_contents|
371 // and |form_contents|.
372 EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());
373 EXPECT_EQ(form_url, form_contents->GetLastCommittedURL());
374 EXPECT_NE(target_contents->GetMainFrame()->GetProcess()->GetID(),
375 form_contents->GetMainFrame()->GetProcess()->GetID());
376
377 // Prepare a file to upload.
378 base::ScopedAllowBlockingForTesting allow_blocking;
379 base::ScopedTempDir temp_dir;
380 base::FilePath file_path;
381 std::string file_content("test-file-content");
382 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
383 ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
384 ASSERT_LT(
385 0, base::WriteFile(file_path, file_content.data(), file_content.size()));
386
387 base::RunLoop run_loop;
388 // Fill out the form to refer to the test file.
389 std::unique_ptr<FileChooserDelegate> delegate(
390 new FileChooserDelegate(file_path, run_loop.QuitClosure()));
391 form_contents->Focus();
392 form_contents->SetDelegate(delegate.get());
393 EXPECT_TRUE(
394 ExecuteScript(form_contents, "document.getElementById('file').click();"));
395 run_loop.Run();
396 ChildProcessSecurityPolicyImpl* security_policy =
397 ChildProcessSecurityPolicyImpl::GetInstance();
398 EXPECT_TRUE(security_policy->CanReadFile(
399 form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
400
401 // Simulate a malicious situation, where the renderer doesn't really have
402 // access to the file.
403 security_policy->RevokeAllPermissionsForFile(
404 form_contents->GetMainFrame()->GetProcess()->GetID(), file_path);
405 EXPECT_FALSE(security_policy->CanReadFile(
406 form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
407 EXPECT_FALSE(security_policy->CanReadFile(
408 target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
409
410 // Submit the form and wait until the malicious renderer gets killed.
411 RenderProcessHostBadIpcMessageWaiter kill_waiter(
412 form_contents->GetMainFrame()->GetProcess());
413 EXPECT_TRUE(ExecuteScript(
414 form_contents,
415 "setTimeout(\n"
416 " function() { document.getElementById('file-form').submit(); },\n"
417 " 0);"));
418 EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
419
420 // The target frame should still be at the original location - the malicious
421 // navigation should have been stopped.
422 EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());
423
424 // Both processes still shouldn't have access.
425 EXPECT_FALSE(security_policy->CanReadFile(
426 form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
427 EXPECT_FALSE(security_policy->CanReadFile(
428 target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
429 }
430
431 // Regression test for https://crbug.com/538784 -- ensures that one can't
432 // sidestep cross-process navigation by detaching a frame mid-request.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,NoDeliveryToDetachedFrame)433 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoDeliveryToDetachedFrame) {
434 GURL attacker_page = embedded_test_server()->GetURL(
435 "evil.com", "/cross_site_iframe_factory.html?evil(evil)");
436 EXPECT_TRUE(NavigateToURL(shell(), attacker_page));
437
438 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
439 ->GetFrameTree()
440 ->root();
441
442 RenderFrameHost* child_frame = root->child_at(0)->current_frame_host();
443
444 // Attacker initiates a navigation to a cross-site document. Under --site-per-
445 // process, these bytes must not be sent to the attacker process.
446 GURL target_resource =
447 embedded_test_server()->GetURL("a.com", "/title1.html");
448 TestNavigationManager target_navigation(shell()->web_contents(),
449 target_resource);
450 EXPECT_TRUE(ExecuteScript(
451 shell()->web_contents()->GetMainFrame(),
452 base::StringPrintf("document.getElementById('child-0').src='%s'",
453 target_resource.spec().c_str())));
454
455 // Wait for the navigation to start.
456 EXPECT_TRUE(target_navigation.WaitForRequestStart());
457 target_navigation.ResumeNavigation();
458
459 // Inject a frame detach message. An attacker-controlled renderer could do
460 // this without also cancelling the pending navigation (as blink would, if you
461 // removed the iframe from the document via js).
462 child_frame->OnMessageReceived(
463 FrameHostMsg_Detach(child_frame->GetRoutingID()));
464
465 // This should cancel the navigation.
466 EXPECT_FALSE(target_navigation.WaitForResponse())
467 << "Request should have been cancelled before reaching the renderer.";
468 }
469
470 } // namespace content
471