blob: 2bb9bfddbe6bd30c41405cb7864debadb169cb19 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame_messages.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/resource_dispatcher_host_delegate.h"
#include "content/public/browser/resource_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.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/navigation_handle_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/escape.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_status.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "url/gurl.h"
namespace content {
// WebContentsDelegate that fails to open a URL when there's a request that
// needs to be transferred between renderers.
class NoTransferRequestDelegate : public WebContentsDelegate {
public:
NoTransferRequestDelegate() {}
bool ShouldTransferNavigation(bool is_main_frame_navigation) override {
// Intentionally cancel the transfer.
return false;
}
private:
DISALLOW_COPY_AND_ASSIGN(NoTransferRequestDelegate);
};
class CrossSiteTransferTest : public ContentBrowserTest {
public:
CrossSiteTransferTest() {}
// ContentBrowserTest implementation:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
protected:
void NavigateToURLContentInitiated(Shell* window,
const GURL& url,
bool should_replace_current_entry,
bool should_wait_for_navigation) {
std::unique_ptr<TestNavigationManager> navigation_manager =
should_wait_for_navigation
? std::unique_ptr<TestNavigationManager>(
new TestNavigationManager(window->web_contents(), url))
: nullptr;
std::string script;
if (should_replace_current_entry)
script = base::StringPrintf("location.replace('%s')", url.spec().c_str());
else
script = base::StringPrintf("location.href = '%s'", url.spec().c_str());
bool result = ExecuteScript(window, script);
EXPECT_TRUE(result);
if (should_wait_for_navigation) {
EXPECT_TRUE(navigation_manager->WaitForRequestStart());
EXPECT_TRUE(navigation_manager->WaitForResponse());
navigation_manager->WaitForNavigationFinished();
EXPECT_TRUE(navigation_manager->was_successful());
}
}
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
};
// The following tests crash in the ThreadSanitizer runtime,
// http://crbug.com/356758.
#if defined(THREAD_SANITIZER)
#define MAYBE_ReplaceEntryCrossProcessThenTransfer \
DISABLED_ReplaceEntryCrossProcessThenTransfer
#define MAYBE_ReplaceEntryCrossProcessTwice \
DISABLED_ReplaceEntryCrossProcessTwice
#else
#define MAYBE_ReplaceEntryCrossProcessThenTransfer \
ReplaceEntryCrossProcessThenTransfer
#define MAYBE_ReplaceEntryCrossProcessTwice ReplaceEntryCrossProcessTwice
#endif
// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that began with a cross-process navigation.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
MAYBE_ReplaceEntryCrossProcessThenTransfer) {
NavigationController& controller = shell()->web_contents()->GetController();
// Navigate to a starting URL, so there is a history entry to replace.
GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
EXPECT_TRUE(NavigateToURL(shell(), url1));
// Navigate to a page on A.com with entry replacement. This navigation is
// cross-site, so the renderer will send it to the browser via OpenURL to give
// to a new process. It will then be transferred into yet another process due
// to the call above.
GURL url2 =
embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
NavigateToURLContentInitiated(shell(), url2, true, true);
// There should be one history entry. url2 should have replaced url1.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
// Now navigate as before to a page on B.com, but normally (without
// replacement). This will still perform a double process-swap as above, via
// OpenURL and then transfer.
GURL url3 =
embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
NavigateToURLContentInitiated(shell(), url3, false, true);
// There should be two history entries. url2 should have replaced url1. url2
// should not have replaced url3.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
}
// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that began with a content-initiated in-process
// navigation. This test is the same as the test above, except transfering from
// in-process.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
ReplaceEntryInProcessThenTransfer) {
NavigationController& controller = shell()->web_contents()->GetController();
// Navigate to a starting URL, so there is a history entry to replace.
GURL url = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
EXPECT_TRUE(NavigateToURL(shell(), url));
// Navigate in-process with entry replacement. It will then be transferred
// into a new one due to the call above.
GURL url2 = embedded_test_server()->GetURL("/site_isolation/blank.html?2");
NavigateToURLContentInitiated(shell(), url2, true, true);
// There should be one history entry. url2 should have replaced url1.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
// Now navigate as before, but without replacement.
GURL url3 = embedded_test_server()->GetURL("/site_isolation/blank.html?3");
NavigateToURLContentInitiated(shell(), url3, false, true);
// There should be two history entries. url2 should have replaced url1. url2
// should not have replaced url3.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
}
// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that cross processes twice from renderer policy.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
MAYBE_ReplaceEntryCrossProcessTwice) {
NavigationController& controller = shell()->web_contents()->GetController();
// Navigate to a starting URL, so there is a history entry to replace.
GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
EXPECT_TRUE(NavigateToURL(shell(), url1));
// Navigate to a page on A.com which redirects to B.com with entry
// replacement. This will switch processes via OpenURL twice. First to A.com,
// and second in response to the server redirect to B.com. The second swap is
// also renderer-initiated via OpenURL because decidePolicyForNavigation is
// currently applied on redirects.
GURL::Replacements replace_host;
GURL url2b =
embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?2");
GURL url2a = embedded_test_server()->GetURL(
"A.com", "/cross-site/" + url2b.host() + url2b.PathForRequest());
NavigateToURLContentInitiated(shell(), url2a, true, true);
// There should be one history entry. url2b should have replaced url1.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
// Now repeat without replacement.
GURL url3b =
embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
GURL url3a = embedded_test_server()->GetURL(
"A.com", "/cross-site/" + url3b.host() + url3b.PathForRequest());
NavigateToURLContentInitiated(shell(), url3a, false, true);
// There should be two history entries. url2b should have replaced url1. url3b
// should not have replaced url2b.
EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(url3b, controller.GetEntryAtIndex(1)->GetURL());
}
// Tests that the request is destroyed when a cross process navigation is
// cancelled.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoLeakOnCrossSiteCancel) {
NavigationController& controller = shell()->web_contents()->GetController();
// Navigate to a starting URL, so there is a history entry to replace.
GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
EXPECT_TRUE(NavigateToURL(shell(), url1));
NoTransferRequestDelegate no_transfer_request_delegate;
WebContentsDelegate* old_delegate = shell()->web_contents()->GetDelegate();
shell()->web_contents()->SetDelegate(&no_transfer_request_delegate);
// Navigate to a page on A.com with entry replacement. This navigation is
// cross-site, so the renderer will send it to the browser via OpenURL to give
// to a new process. It will then be transferred into yet another process due
// to the call above.
GURL url2 =
embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
TestNavigationManager navigation_manager(shell()->web_contents(), url2);
NavigationHandleObserver handle_observer(shell()->web_contents(), url2);
// Don't wait for the navigation to complete, since that never happens in
// this case.
NavigateToURLContentInitiated(shell(), url2, false, false);
// Make sure the request for url2 did not complete.
EXPECT_FALSE(navigation_manager.WaitForResponse());
// There should be one history entry, with url1.
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code());
shell()->web_contents()->SetDelegate(old_delegate);
}
// Test that verifies that a cross-process transfer retains ability to read
// files encapsulated by HTTP POST body that is forwarded to the new renderer.
// Invalid handling of this scenario has been suspected as the cause of at least
// some of the renderer kills tracked in https://crbug.com/613260.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, PostWithFileData) {
// Navigate to the page with form that posts via 307 redirection to
// |redirect_target_url| (cross-site from |form_url|). Using 307 (rather than
// 302) redirection is important to preserve the HTTP method and POST body.
GURL form_url(embedded_test_server()->GetURL(
"a.com", "/form_that_posts_cross_site.html"));
GURL redirect_target_url(embedded_test_server()->GetURL("x.com", "/echoall"));
EXPECT_TRUE(NavigateToURL(shell(), form_url));
// Prepare a file to upload.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
base::FilePath file_path;
std::string file_content("test-file-content");
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
ASSERT_LT(
0, base::WriteFile(file_path, file_content.data(), file_content.size()));
base::RunLoop run_loop;
// Fill out the form to refer to the test file.
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file_path, run_loop.QuitClosure()));
shell()->web_contents()->SetDelegate(delegate.get());
EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
"document.getElementById('file').click();"));
run_loop.Run();
// Remember the old process id for a sanity check below.
int old_process_id =
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();
// Submit the form.
TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
EXPECT_TRUE(
ExecuteScript(shell(), "document.getElementById('file-form').submit();"));
form_post_observer.Wait();
// Verify that we arrived at the expected, redirected location.
EXPECT_EQ(redirect_target_url,
shell()->web_contents()->GetLastCommittedURL());
// Verify that the test really verifies access of a *new* renderer process.
int new_process_id =
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();
ASSERT_NE(new_process_id, old_process_id);
// MAIN VERIFICATION: Check if the new renderer process is able to read the
// file.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
new_process_id, file_path));
// Verify that POST body got preserved by 307 redirect. This expectation
// comes from: https://tools.ietf.org/html/rfc7231#section-6.4.7
std::string actual_page_body;
EXPECT_TRUE(ExecuteScriptAndExtractString(
shell()->web_contents(),
"window.domAutomationController.send("
"document.getElementsByTagName('pre')[0].innerText);",
&actual_page_body));
EXPECT_THAT(actual_page_body, ::testing::HasSubstr(file_content));
EXPECT_THAT(actual_page_body,
::testing::HasSubstr(file_path.BaseName().AsUTF8Unsafe()));
EXPECT_THAT(actual_page_body,
::testing::HasSubstr("form-data; name=\"file\""));
}
// Test that verifies that if navigation originator doesn't have access to a
// file, then no access is granted after a cross-process transfer of POST data.
// This is a regression test for https://crbug.com/726067.
//
// This test is somewhat similar to
// http/tests/navigation/form-targets-cross-site-frame-post.html web test
// except that it 1) tests with files, 2) simulates a malicious scenario and 3)
// verifies file access (all of these 3 things are not possible with web
// tests).
//
// This test is very similar to CrossSiteTransferTest.PostWithFileData above,
// except that it simulates a malicious form / POST originator.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, MaliciousPostWithFileData) {
// The initial test window is a named form target.
GURL initial_target_url(
embedded_test_server()->GetURL("initial-target.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), initial_target_url));
WebContents* target_contents = shell()->web_contents();
EXPECT_TRUE(ExecuteScript(target_contents, "window.name = 'form-target';"));
// Create a new window containing a form targeting |target_contents|.
GURL form_url(embedded_test_server()->GetURL(
"main.com", "/form_that_posts_cross_site.html"));
Shell* other_window = OpenPopup(target_contents, form_url, "form-window");
WebContents* form_contents = other_window->web_contents();
EXPECT_TRUE(ExecuteScript(
form_contents,
"document.getElementById('file-form').target = 'form-target';"));
// Verify the current locations and process placement of |target_contents|
// and |form_contents|.
EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());
EXPECT_EQ(form_url, form_contents->GetLastCommittedURL());
EXPECT_NE(target_contents->GetMainFrame()->GetProcess()->GetID(),
form_contents->GetMainFrame()->GetProcess()->GetID());
// Prepare a file to upload.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
base::FilePath file_path;
std::string file_content("test-file-content");
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
ASSERT_LT(
0, base::WriteFile(file_path, file_content.data(), file_content.size()));
base::RunLoop run_loop;
// Fill out the form to refer to the test file.
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file_path, run_loop.QuitClosure()));
form_contents->Focus();
form_contents->SetDelegate(delegate.get());
EXPECT_TRUE(
ExecuteScript(form_contents, "document.getElementById('file').click();"));
run_loop.Run();
ChildProcessSecurityPolicyImpl* security_policy =
ChildProcessSecurityPolicyImpl::GetInstance();
EXPECT_TRUE(security_policy->CanReadFile(
form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
// Simulate a malicious situation, where the renderer doesn't really have
// access to the file.
security_policy->RevokeAllPermissionsForFile(
form_contents->GetMainFrame()->GetProcess()->GetID(), file_path);
EXPECT_FALSE(security_policy->CanReadFile(
form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
EXPECT_FALSE(security_policy->CanReadFile(
target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
// Submit the form and wait until the malicious renderer gets killed.
RenderProcessHostKillWaiter kill_waiter(
form_contents->GetMainFrame()->GetProcess());
EXPECT_TRUE(ExecuteScript(
form_contents,
"setTimeout(\n"
" function() { document.getElementById('file-form').submit(); },\n"
" 0);"));
EXPECT_EQ(bad_message::RFPH_ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
// The target frame should still be at the original location - the malicious
// navigation should have been stopped.
EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());
// Both processes still shouldn't have access.
EXPECT_FALSE(security_policy->CanReadFile(
form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
EXPECT_FALSE(security_policy->CanReadFile(
target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
}
// Regression test for https://crbug.com/538784 -- ensures that one can't
// sidestep cross-process navigation by detaching a frame mid-request.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoDeliveryToDetachedFrame) {
GURL attacker_page = embedded_test_server()->GetURL(
"evil.com", "/cross_site_iframe_factory.html?evil(evil)");
EXPECT_TRUE(NavigateToURL(shell(), attacker_page));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
RenderFrameHost* child_frame = root->child_at(0)->current_frame_host();
// Attacker initiates a navigation to a cross-site document. Under --site-per-
// process, these bytes must not be sent to the attacker process.
GURL target_resource =
embedded_test_server()->GetURL("a.com", "/title1.html");
TestNavigationManager target_navigation(shell()->web_contents(),
target_resource);
EXPECT_TRUE(ExecuteScript(
shell()->web_contents()->GetMainFrame(),
base::StringPrintf("document.getElementById('child-0').src='%s'",
target_resource.spec().c_str())));
// Wait for the navigation to start.
EXPECT_TRUE(target_navigation.WaitForRequestStart());
target_navigation.ResumeNavigation();
// Inject a frame detach message. An attacker-controlled renderer could do
// this without also cancelling the pending navigation (as blink would, if you
// removed the iframe from the document via js).
child_frame->OnMessageReceived(
FrameHostMsg_Detach(child_frame->GetRoutingID()));
// This should cancel the navigation.
EXPECT_FALSE(target_navigation.WaitForResponse())
<< "Request should have been cancelled before reaching the renderer.";
}
} // namespace content