1 // Copyright (c) 2013 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 <stdint.h>
6
7 #include "base/bind.h"
8 #include "base/command_line.h"
9 #include "base/feature_list.h"
10 #include "base/files/file_util.h"
11 #include "base/macros.h"
12 #include "base/memory/ptr_util.h"
13 #include "base/optional.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/test/scoped_feature_list.h"
16 #include "build/build_config.h"
17 #include "content/browser/bad_message.h"
18 #include "content/browser/child_process_security_policy_impl.h"
19 #include "content/browser/dom_storage/dom_storage_context_wrapper.h"
20 #include "content/browser/dom_storage/session_storage_namespace_impl.h"
21 #include "content/browser/renderer_host/navigator.h"
22 #include "content/browser/renderer_host/render_frame_host_impl.h"
23 #include "content/browser/renderer_host/render_frame_proxy_host.h"
24 #include "content/browser/renderer_host/render_process_host_impl.h"
25 #include "content/browser/renderer_host/render_view_host_factory.h"
26 #include "content/browser/renderer_host/render_view_host_impl.h"
27 #include "content/browser/web_contents/file_chooser_impl.h"
28 #include "content/browser/web_contents/web_contents_impl.h"
29 #include "content/common/frame.mojom.h"
30 #include "content/common/frame_messages.h"
31 #include "content/common/frame_proxy.mojom-test-utils.h"
32 #include "content/common/render_message_filter.mojom.h"
33 #include "content/public/browser/blob_handle.h"
34 #include "content/public/browser/browser_context.h"
35 #include "content/public/browser/browser_task_traits.h"
36 #include "content/public/browser/browser_thread.h"
37 #include "content/public/browser/content_browser_client.h"
38 #include "content/public/browser/file_select_listener.h"
39 #include "content/public/browser/navigation_handle.h"
40 #include "content/public/browser/resource_context.h"
41 #include "content/public/browser/storage_partition.h"
42 #include "content/public/common/bindings_policy.h"
43 #include "content/public/common/content_features.h"
44 #include "content/public/common/content_switches.h"
45 #include "content/public/common/navigation_policy.h"
46 #include "content/public/common/url_constants.h"
47 #include "content/public/test/back_forward_cache_util.h"
48 #include "content/public/test/browser_test.h"
49 #include "content/public/test/browser_test_utils.h"
50 #include "content/public/test/content_browser_test.h"
51 #include "content/public/test/content_browser_test_utils.h"
52 #include "content/public/test/navigation_handle_observer.h"
53 #include "content/public/test/test_navigation_observer.h"
54 #include "content/public/test/test_renderer_host.h"
55 #include "content/public/test/test_utils.h"
56 #include "content/shell/browser/shell.h"
57 #include "content/test/content_browser_test_utils_internal.h"
58 #include "content/test/did_commit_navigation_interceptor.h"
59 #include "content/test/frame_host_interceptor.h"
60 #include "content/test/test_content_browser_client.h"
61 #include "ipc/ipc_message.h"
62 #include "ipc/ipc_security_test_util.h"
63 #include "mojo/core/embedder/embedder.h"
64 #include "mojo/public/cpp/bindings/pending_associated_remote.h"
65 #include "mojo/public/cpp/bindings/pending_receiver.h"
66 #include "mojo/public/cpp/bindings/pending_remote.h"
67 #include "mojo/public/cpp/bindings/remote.h"
68 #include "mojo/public/cpp/bindings/self_owned_associated_receiver.h"
69 #include "mojo/public/cpp/test_support/test_utils.h"
70 #include "net/base/filename_util.h"
71 #include "net/base/network_isolation_key.h"
72 #include "net/dns/mock_host_resolver.h"
73 #include "net/test/embedded_test_server/controllable_http_response.h"
74 #include "net/test/embedded_test_server/embedded_test_server.h"
75 #include "net/test/embedded_test_server/http_request.h"
76 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
77 #include "services/network/public/cpp/features.h"
78 #include "services/network/public/cpp/network_switches.h"
79 #include "services/network/public/cpp/resource_request.h"
80 #include "services/network/public/mojom/trust_tokens.mojom.h"
81 #include "services/network/public/mojom/url_loader.mojom.h"
82 #include "services/network/test/test_url_loader_client.h"
83 #include "storage/browser/blob/blob_registry_impl.h"
84 #include "testing/gmock/include/gmock/gmock.h"
85 #include "testing/gtest/include/gtest/gtest.h"
86 #include "third_party/blink/public/common/blob/blob_utils.h"
87 #include "third_party/blink/public/common/navigation/triggering_event_info.h"
88 #include "third_party/blink/public/mojom/appcache/appcache.mojom.h"
89 #include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h"
90 #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
91 #include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
92
93 using IPC::IpcSecurityTestUtil;
94 using ::testing::HasSubstr;
95 using ::testing::Optional;
96
97 namespace content {
98
99 namespace {
100
101 // This is a helper function for the tests which attempt to create a
102 // duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects
103 // with the same process and routing ids, which causes a collision.
104 // It creates a couple of windows in process 1, which causes a few routing ids
105 // to be allocated. Then a cross-process navigation is initiated, which causes a
106 // new process 2 to be created and have a pending RenderViewHost for it. The
107 // routing id of the RenderViewHost which is target for a duplicate is set
108 // into |target_routing_id| and the pending RenderFrameHost which is used for
109 // the attempt is the return value.
PrepareToDuplicateHosts(Shell * shell,net::EmbeddedTestServer * server,int * target_routing_id)110 RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell,
111 net::EmbeddedTestServer* server,
112 int* target_routing_id) {
113 GURL foo("http://foo.com/simple_page.html");
114
115 if (AreDefaultSiteInstancesEnabled()) {
116 // Isolate "bar.com" so we are guaranteed to get a different process
117 // for navigations to this origin.
118 IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"});
119 }
120
121 // Start off with initial navigation, so we get the first process allocated.
122 EXPECT_TRUE(NavigateToURL(shell, foo));
123 EXPECT_EQ(base::ASCIIToUTF16("OK"), shell->web_contents()->GetTitle());
124
125 // Open another window, so we generate some more routing ids.
126 ShellAddedObserver shell2_observer;
127 EXPECT_TRUE(ExecuteScript(shell, "window.open(document.URL + '#2');"));
128 Shell* shell2 = shell2_observer.GetShell();
129
130 // The new window must be in the same process, but have a new routing id.
131 EXPECT_EQ(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(),
132 shell2->web_contents()->GetMainFrame()->GetProcess()->GetID());
133 *target_routing_id =
134 shell2->web_contents()->GetRenderViewHost()->GetRoutingID();
135 EXPECT_NE(*target_routing_id,
136 shell->web_contents()->GetRenderViewHost()->GetRoutingID());
137
138 // Now, simulate a link click coming from the renderer.
139 GURL extension_url("http://bar.com/simple_page.html");
140 WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell->web_contents());
141 wc->GetFrameTree()->root()->navigator().RequestOpenURL(
142 wc->GetFrameTree()->root()->current_frame_host(), extension_url,
143 GlobalFrameRoutingId() /* initiator_routing_id */,
144 url::Origin::Create(foo), nullptr, std::string(), Referrer(),
145 WindowOpenDisposition::CURRENT_TAB, false, true,
146 blink::TriggeringEventInfo::kFromTrustedEvent, std::string(),
147 nullptr /* blob_url_loader_factory */, base::nullopt /* impression */);
148
149 // Since the navigation above requires a cross-process swap, there will be a
150 // speculative/pending RenderFrameHost. Ensure it exists and is in a different
151 // process than the initial page.
152 RenderFrameHostImpl* next_rfh =
153 wc->GetRenderManagerForTesting()->speculative_frame_host();
154
155 EXPECT_TRUE(next_rfh);
156 EXPECT_NE(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(),
157 next_rfh->GetProcess()->GetID());
158
159 return next_rfh;
160 }
161
CreateOpenURLParams(const GURL & url)162 content::mojom::OpenURLParamsPtr CreateOpenURLParams(const GURL& url) {
163 auto params = mojom::OpenURLParams::New();
164 params->url = url;
165 params->disposition = WindowOpenDisposition::CURRENT_TAB;
166 params->should_replace_current_entry = false;
167 params->user_gesture = true;
168 return params;
169 }
170
CreateMemoryBackedBlob(BrowserContext * browser_context,const std::string & contents,const std::string & content_type)171 std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
172 BrowserContext* browser_context,
173 const std::string& contents,
174 const std::string& content_type) {
175 std::unique_ptr<content::BlobHandle> result;
176 base::RunLoop loop;
177 BrowserContext::CreateMemoryBackedBlob(
178 browser_context, base::as_bytes(base::make_span(contents)), content_type,
179 base::BindOnce(
180 [](std::unique_ptr<content::BlobHandle>* out_blob,
181 base::OnceClosure done,
182 std::unique_ptr<content::BlobHandle> blob) {
183 *out_blob = std::move(blob);
184 std::move(done).Run();
185 },
186 &result, loop.QuitClosure()));
187 loop.Run();
188 EXPECT_TRUE(result);
189 return result;
190 }
191
192 // Helper class to interpose on Blob URL registrations, replacing the URL
193 // contained in incoming registration requests with the specified URL.
194 class BlobURLStoreInterceptor
195 : public blink::mojom::BlobURLStoreInterceptorForTesting {
196 public:
Intercept(GURL target_url,mojo::SelfOwnedAssociatedReceiverRef<blink::mojom::BlobURLStore> receiver)197 static void Intercept(
198 GURL target_url,
199 mojo::SelfOwnedAssociatedReceiverRef<blink::mojom::BlobURLStore>
200 receiver) {
201 auto interceptor =
202 base::WrapUnique(new BlobURLStoreInterceptor(target_url));
203 auto* raw_interceptor = interceptor.get();
204 auto impl = receiver->SwapImplForTesting(std::move(interceptor));
205 raw_interceptor->url_store_ = std::move(impl);
206 }
207
GetForwardingInterface()208 blink::mojom::BlobURLStore* GetForwardingInterface() override {
209 return url_store_.get();
210 }
211
Register(mojo::PendingRemote<blink::mojom::Blob> blob,const GURL & url,RegisterCallback callback)212 void Register(mojo::PendingRemote<blink::mojom::Blob> blob,
213 const GURL& url,
214 RegisterCallback callback) override {
215 GetForwardingInterface()->Register(std::move(blob), target_url_,
216 std::move(callback));
217 }
218
219 private:
BlobURLStoreInterceptor(GURL target_url)220 explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {}
221
222 std::unique_ptr<blink::mojom::BlobURLStore> url_store_;
223 GURL target_url_;
224 };
225
226 // Constructs a WebContentsDelegate that mocks a file dialog.
227 // Unlike content::FileChooserDelegate, this class doesn't make a response in
228 // RunFileChooser(), and a user needs to call Choose().
229 class DelayedFileChooserDelegate : public WebContentsDelegate {
230 public:
Choose(const base::FilePath & file)231 void Choose(const base::FilePath& file) {
232 auto file_info = blink::mojom::FileChooserFileInfo::NewNativeFile(
233 blink::mojom::NativeFileInfo::New(file, base::string16()));
234 std::vector<blink::mojom::FileChooserFileInfoPtr> files;
235 files.push_back(std::move(file_info));
236 listener_->FileSelected(std::move(files), base::FilePath(),
237 blink::mojom::FileChooserParams::Mode::kOpen);
238 listener_.reset();
239 }
240
241 // WebContentsDelegate overrides
RunFileChooser(RenderFrameHost * render_frame_host,scoped_refptr<FileSelectListener> listener,const blink::mojom::FileChooserParams & params)242 void RunFileChooser(RenderFrameHost* render_frame_host,
243 scoped_refptr<FileSelectListener> listener,
244 const blink::mojom::FileChooserParams& params) override {
245 listener_ = std::move(listener);
246 }
247
EnumerateDirectory(WebContents * web_contents,scoped_refptr<FileSelectListener> listener,const base::FilePath & directory_path)248 void EnumerateDirectory(WebContents* web_contents,
249 scoped_refptr<FileSelectListener> listener,
250 const base::FilePath& directory_path) override {
251 listener->FileSelectionCanceled();
252 }
253
254 private:
255 scoped_refptr<FileSelectListener> listener_;
256 };
257
FileChooserCallback(base::RunLoop * run_loop,blink::mojom::FileChooserResultPtr result)258 void FileChooserCallback(base::RunLoop* run_loop,
259 blink::mojom::FileChooserResultPtr result) {
260 run_loop->Quit();
261 }
262
263 } // namespace
264
265 // The goal of these tests will be to "simulate" exploited renderer processes,
266 // which can send arbitrary IPC messages and confuse browser process internal
267 // state, leading to security bugs. We are trying to verify that the browser
268 // doesn't perform any dangerous operations in such cases.
269 class SecurityExploitBrowserTest : public ContentBrowserTest {
270 public:
SecurityExploitBrowserTest()271 SecurityExploitBrowserTest() {}
272
SetUpCommandLine(base::CommandLine * command_line)273 void SetUpCommandLine(base::CommandLine* command_line) override {
274 // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
275 // which is required below. This cannot invoke Start() however as that kicks
276 // off the "EmbeddedTestServer IO Thread" which then races with
277 // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
278 ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
279
280 // Add a host resolver rule to map all outgoing requests to the test server.
281 // This allows us to use "real" hostnames in URLs, which we can use to
282 // create arbitrary SiteInstances.
283 command_line->AppendSwitchASCII(
284 network::switches::kHostResolverRules,
285 "MAP * " +
286 net::HostPortPair::FromURL(embedded_test_server()->base_url())
287 .ToString() +
288 ",EXCLUDE localhost");
289 }
290
SetUpOnMainThread()291 void SetUpOnMainThread() override {
292 // Complete the manual Start() after ContentBrowserTest's own
293 // initialization, ref. comment on InitializeAndListen() above.
294 embedded_test_server()->StartAcceptingConnections();
295 }
296
297 protected:
298 // Tests that a given file path sent in a FrameHostMsg_RunFileChooser will
299 // cause renderer to be killed.
300 void TestFileChooserWithPath(const base::FilePath& path);
301
IsolateOrigin(const std::string & hostname)302 void IsolateOrigin(const std::string& hostname) {
303 IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
304 {hostname});
305 }
306 };
307
TestFileChooserWithPath(const base::FilePath & path)308 void SecurityExploitBrowserTest::TestFileChooserWithPath(
309 const base::FilePath& path) {
310 GURL foo("http://foo.com/simple_page.html");
311 EXPECT_TRUE(NavigateToURL(shell(), foo));
312 EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle());
313
314 RenderFrameHost* compromised_renderer =
315 shell()->web_contents()->GetMainFrame();
316 blink::mojom::FileChooserParamsPtr params =
317 blink::mojom::FileChooserParams::New();
318 params->default_file_name = path;
319
320 mojo::test::BadMessageObserver bad_message_observer;
321 mojo::Remote<blink::mojom::FileChooser> chooser =
322 FileChooserImpl::CreateBoundForTesting(
323 static_cast<RenderFrameHostImpl*>(compromised_renderer));
324 chooser->OpenFileChooser(
325 std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback());
326 chooser.FlushForTesting();
327 EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
328 ::testing::StartsWith("FileChooser: The default file name"));
329 }
330
331 // Ensure that we kill the renderer process if we try to give it WebUI
332 // properties and it doesn't have enabled WebUI bindings.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,SetWebUIProperty)333 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, SetWebUIProperty) {
334 GURL foo("http://foo.com/simple_page.html");
335
336 EXPECT_TRUE(NavigateToURL(shell(), foo));
337 EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle());
338 EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
339
340 RenderFrameHost* compromised_renderer =
341 shell()->web_contents()->GetMainFrame();
342 RenderProcessHostBadIpcMessageWaiter kill_waiter(
343 compromised_renderer->GetProcess());
344 compromised_renderer->SetWebUIProperty("toolkit", "views");
345 EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait());
346 }
347
348 // This is a test for crbug.com/312016 attempting to create duplicate
349 // RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves
350 // it in a state with pending RenderViewHost. Before the commit of the new
351 // pending RenderViewHost, this test case creates a new window through the new
352 // process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,AttemptDuplicateRenderViewHost)353 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
354 AttemptDuplicateRenderViewHost) {
355 int32_t duplicate_routing_id = MSG_ROUTING_NONE;
356 RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
357 shell(), embedded_test_server(), &duplicate_routing_id);
358 EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id);
359
360 mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
361 params->target_url = GURL("about:blank");
362 pending_rfh->CreateNewWindow(
363 std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus,
364 mojom::CreateNewWindowReplyPtr) {}));
365 // If the above operation doesn't cause a crash, the test has succeeded!
366 }
367
368 // This is a test for crbug.com/312016. It tries to create two RenderWidgetHosts
369 // with the same process and routing ids, which causes a collision. It is almost
370 // identical to the AttemptDuplicateRenderViewHost test case.
371 // Crashes on all platforms. http://crbug.com/939338
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,DISABLED_AttemptDuplicateRenderWidgetHost)372 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
373 DISABLED_AttemptDuplicateRenderWidgetHost) {
374 int duplicate_routing_id = MSG_ROUTING_NONE;
375 RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
376 shell(), embedded_test_server(), &duplicate_routing_id);
377 EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id);
378
379 // Since this test executes on the UI thread and hopping threads might cause
380 // different timing in the test, let's simulate a CreateNewWidget call coming
381 // from the IO thread. Use the existing window routing id to cause a
382 // deliberate collision.
383 pending_rfh->CreateNewPopupWidget(
384 mojo::PendingAssociatedReceiver<blink::mojom::PopupWidgetHost>(),
385 mojo::PendingAssociatedReceiver<blink::mojom::WidgetHost>(),
386 mojo::PendingAssociatedRemote<blink::mojom::Widget>());
387
388 // If the above operation doesn't crash, the test has succeeded!
389 }
390
391 // This is a test for crbug.com/444198. It tries to send a
392 // FrameHostMsg_RunFileChooser containing an invalid path. The browser should
393 // correctly terminate the renderer in these cases.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,AttemptRunFileChoosers)394 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptRunFileChoosers) {
395 TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("../../*.txt")));
396 TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("/etc/*.conf")));
397 #if defined(OS_WIN)
398 TestFileChooserWithPath(
399 base::FilePath(FILE_PATH_LITERAL("\\\\evilserver\\evilshare\\*.txt")));
400 TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("c:\\*.txt")));
401 TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("..\\..\\*.txt")));
402 #endif
403 }
404
405 // A test for crbug.com/941008.
406 // Calling OpenFileChooser() and EnumerateChosenDirectory() for a single
407 // FileChooser instance had a problem.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,UnexpectedMethodsSequence)408 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, UnexpectedMethodsSequence) {
409 EXPECT_TRUE(NavigateToURL(shell(), GURL("http://foo.com/simple_page.html")));
410 RenderFrameHost* compromised_renderer =
411 shell()->web_contents()->GetMainFrame();
412 auto delegate = std::make_unique<DelayedFileChooserDelegate>();
413 shell()->web_contents()->SetDelegate(delegate.get());
414
415 mojo::Remote<blink::mojom::FileChooser> chooser =
416 FileChooserImpl::CreateBoundForTesting(
417 static_cast<RenderFrameHostImpl*>(compromised_renderer));
418 base::RunLoop run_loop1;
419 base::RunLoop run_loop2;
420 chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
421 base::BindOnce(FileChooserCallback, &run_loop2));
422 // The following EnumerateChosenDirectory() runs the specified callback
423 // immediately regardless of the content of the first argument FilePath.
424 chooser->EnumerateChosenDirectory(
425 base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")),
426 base::BindOnce(FileChooserCallback, &run_loop1));
427 run_loop1.Run();
428
429 delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt")));
430 run_loop2.Run();
431
432 // The test passes if it doesn't crash.
433 }
434
435 class CorsExploitBrowserTest : public ContentBrowserTest {
436 public:
437 CorsExploitBrowserTest() = default;
438
SetUpOnMainThread()439 void SetUpOnMainThread() override {
440 host_resolver()->AddRule("*", "127.0.0.1");
441 SetupCrossSiteRedirector(embedded_test_server());
442 }
443
444 private:
445 DISALLOW_COPY_AND_ASSIGN(CorsExploitBrowserTest);
446 };
447
448 // Test that receiving a commit with incorrect origin properly terminates the
449 // renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,MismatchedOriginOnCommit)450 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnCommit) {
451 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
452 EXPECT_TRUE(NavigateToURL(shell(), start_url));
453
454 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
455 ->GetFrameTree()
456 ->root();
457
458 // Navigate to a new URL, with an interceptor that replaces the origin with
459 // one that does not match params.url.
460 GURL url(embedded_test_server()->GetURL("/title2.html"));
461 PwnCommitIPC(shell()->web_contents(), url, url,
462 url::Origin::Create(GURL("http://bar.com/")));
463
464 // Use LoadURL, as the test shouldn't wait for navigation commit.
465 NavigationController& controller = shell()->web_contents()->GetController();
466 controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
467 EXPECT_NE(nullptr, controller.GetPendingEntry());
468 EXPECT_EQ(url, controller.GetPendingEntry()->GetURL());
469
470 RenderProcessHostBadIpcMessageWaiter kill_waiter(
471 root->current_frame_host()->GetProcess());
472
473 // When the IPC message is received and validation fails, the process is
474 // terminated. However, the notification for that should be processed in a
475 // separate task of the message loop, so ensure that the process is still
476 // considered alive.
477 EXPECT_TRUE(
478 root->current_frame_host()->GetProcess()->IsInitializedAndNotDead());
479
480 EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
481 }
482
483 namespace {
484
485 // Interceptor that replaces |interface_params| with the specified
486 // value for the first DidCommitProvisionalLoad message it observes in the given
487 // |web_contents| while in scope.
488 class ScopedInterfaceParamsReplacer : public DidCommitNavigationInterceptor {
489 public:
ScopedInterfaceParamsReplacer(WebContents * web_contents,mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override)490 ScopedInterfaceParamsReplacer(
491 WebContents* web_contents,
492 mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override)
493 : DidCommitNavigationInterceptor(web_contents),
494 params_override_(std::move(params_override)) {}
495 ~ScopedInterfaceParamsReplacer() override = default;
496
497 protected:
WillProcessDidCommitNavigation(RenderFrameHost * render_frame_host,NavigationRequest * navigation_request,::FrameHostMsg_DidCommitProvisionalLoad_Params * params,mojom::DidCommitProvisionalLoadInterfaceParamsPtr * interface_params)498 bool WillProcessDidCommitNavigation(
499 RenderFrameHost* render_frame_host,
500 NavigationRequest* navigation_request,
501 ::FrameHostMsg_DidCommitProvisionalLoad_Params* params,
502 mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
503 override {
504 interface_params->Swap(¶ms_override_);
505
506 return true;
507 }
508
509 private:
510 mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override_;
511
512 DISALLOW_COPY_AND_ASSIGN(ScopedInterfaceParamsReplacer);
513 };
514
515 } // namespace
516
517 // Test that, as a general rule, not receiving new
518 // DidCommitProvisionalLoadInterfaceParamsPtr for a cross-document navigation
519 // properly terminates the renderer process. There is one exception to this
520 // rule, see: RenderFrameHostImplBrowserTest.
521 // InterfaceProviderRequestIsOptionalForFirstCommit.
522 // TODO(crbug.com/718652): when all clients are converted to use
523 // BrowserInterfaceBroker, PendingReceiver<InterfaceProvider>-related code will
524 // be removed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,MissingInterfaceProviderOnNonSameDocumentCommit)525 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
526 MissingInterfaceProviderOnNonSameDocumentCommit) {
527 const GURL start_url(embedded_test_server()->GetURL("/title1.html"));
528 const GURL non_same_document_url(
529 embedded_test_server()->GetURL("/title2.html"));
530
531 EXPECT_TRUE(NavigateToURL(shell(), start_url));
532
533 RenderFrameHostImpl* frame = static_cast<RenderFrameHostImpl*>(
534 shell()->web_contents()->GetMainFrame());
535 RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());
536
537 NavigationHandleObserver navigation_observer(shell()->web_contents(),
538 non_same_document_url);
539 ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr);
540 EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), non_same_document_url));
541 EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait());
542
543 // Verify that the death of the renderer process doesn't leave behind and
544 // leak NavigationRequests - see https://crbug.com/869193.
545 EXPECT_FALSE(frame->HasPendingCommitNavigation());
546 EXPECT_FALSE(navigation_observer.has_committed());
547 EXPECT_TRUE(navigation_observer.is_error());
548 EXPECT_TRUE(navigation_observer.last_committed_url().is_empty());
549 EXPECT_EQ(net::OK, navigation_observer.net_error_code());
550 }
551
552 // Test that a compromised renderer cannot ask to upload an arbitrary file in
553 // OpenURL. This is a regression test for https://crbug.com/726067.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,OpenUrl_ResourceRequestBody)554 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
555 OpenUrl_ResourceRequestBody) {
556 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
557 GURL target_url(embedded_test_server()->GetURL("/echoall"));
558 EXPECT_TRUE(NavigateToURL(shell(), start_url));
559
560 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
561 ->GetFrameTree()
562 ->root();
563
564 RenderProcessHostBadIpcMessageWaiter kill_waiter(
565 root->current_frame_host()->GetProcess());
566
567 // Prepare a file to upload.
568 base::ScopedAllowBlockingForTesting allow_blocking;
569 base::ScopedTempDir temp_dir;
570 base::FilePath file_path;
571 std::string file_content("test-file-content");
572 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
573 ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
574 ASSERT_TRUE(base::WriteFile(file_path, file_content));
575
576 // Simulate an OpenURL Mojo method asking to POST a file that the renderer
577 // shouldn't have access to.
578 auto params = CreateOpenURLParams(target_url);
579 params->post_body = new network::ResourceRequestBody;
580 params->post_body->AppendFileRange(file_path, 0, file_content.size(),
581 base::Time());
582 params->should_replace_current_entry = true;
583
584 static_cast<mojom::FrameHost*>(root->current_frame_host())
585 ->OpenURL(std::move(params));
586
587 // Verify that the malicious navigation did not commit the navigation to
588 // |target_url|.
589 EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL());
590
591 // Verify that the malicious renderer got killed.
592 EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
593 }
594
595 // Test that forging a frame's unique name and commit won't allow changing the
596 // PageState of a cross-process FrameNavigationEntry.
597 // See https://crbug.com/766262.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,PageStateToWrongEntry)598 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateToWrongEntry) {
599 IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
600
601 // Commit a page with nested iframes and a separate cross-process iframe.
602 GURL main_url(embedded_test_server()->GetURL(
603 "a.com", "/cross_site_iframe_factory.html?a(a(a),b)"));
604 EXPECT_TRUE(NavigateToURL(shell(), main_url));
605 NavigationEntryImpl* back_entry = static_cast<NavigationEntryImpl*>(
606 shell()->web_contents()->GetController().GetLastCommittedEntry());
607 int nav_entry_id = back_entry->GetUniqueID();
608
609 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
610 ->GetFrameTree()
611 ->root();
612 FrameTreeNode* child0_0 = root->child_at(0)->child_at(0);
613 std::string child0_0_unique_name = child0_0->unique_name();
614 FrameTreeNode* child1 = root->child_at(1);
615 GURL child1_url = child1->current_url();
616 int child1_pid = child1->current_frame_host()->GetProcess()->GetID();
617 blink::PageState child1_page_state =
618 back_entry->GetFrameEntry(child1)->page_state();
619
620 // Add a history item in the nested frame. It's important to do it there and
621 // not the main frame for the repro to work, since we don't walk the subtree
622 // when navigating back/forward between same document items.
623 TestNavigationObserver fragment_observer(shell()->web_contents());
624 EXPECT_TRUE(ExecuteScript(child0_0, "location.href = '#foo';"));
625 fragment_observer.Wait();
626
627 // Simulate a name change IPC from the nested iframe, matching the cross-site
628 // iframe's unique name.
629 child0_0->SetFrameName("foo", child1->unique_name());
630
631 // Simulate a back navigation from the now renamed nested iframe, which would
632 // put a PageState on the cross-site iframe's FrameNavigationEntry. Forge a
633 // data URL within the PageState that differs from child1_url.
634 std::unique_ptr<FrameHostMsg_DidCommitProvisionalLoad_Params> params =
635 std::make_unique<FrameHostMsg_DidCommitProvisionalLoad_Params>();
636 params->nav_entry_id = nav_entry_id;
637 params->did_create_new_entry = false;
638 params->url = GURL("about:blank");
639 params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
640 params->should_update_history = false;
641 params->gesture = NavigationGestureAuto;
642 params->method = "GET";
643 params->page_state =
644 blink::PageState::CreateFromURL(GURL("data:text/html,foo"));
645 params->origin = url::Origin::Create(GURL("about:blank"));
646 params->embedding_token = base::UnguessableToken::Create();
647
648 mojo::PendingRemote<service_manager::mojom::InterfaceProvider>
649 isolated_interface_provider;
650 static_cast<mojom::FrameHost*>(child0_0->current_frame_host())
651 ->DidCommitProvisionalLoad(
652 std::move(params),
653 mojom::DidCommitProvisionalLoadInterfaceParams::New(
654 isolated_interface_provider.InitWithNewPipeAndPassReceiver(),
655 mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>()
656 .InitWithNewPipeAndPassReceiver()));
657
658 // Make sure we haven't changed the FrameNavigationEntry. An attack would
659 // modify the PageState but leave the SiteInstance as it was.
660 EXPECT_EQ(child1->current_frame_host()->GetSiteInstance(),
661 back_entry->GetFrameEntry(child1)->site_instance());
662 EXPECT_EQ(child1_page_state, back_entry->GetFrameEntry(child1)->page_state());
663
664 // Put the frame's unique name back.
665 child0_0->SetFrameName("bar", child0_0_unique_name);
666
667 // Go forward after the fake back navigation.
668 TestNavigationObserver forward_observer(shell()->web_contents());
669 shell()->web_contents()->GetController().GoForward();
670 forward_observer.Wait();
671
672 // Go back to the possibly corrupted entry and ensure we didn't load the data
673 // URL in the previous process. A test failure here would appear as a failure
674 // of the URL check and not the process ID check.
675 TestNavigationObserver back_observer(shell()->web_contents());
676 shell()->web_contents()->GetController().GoBack();
677 back_observer.Wait();
678 EXPECT_EQ(child1_pid, child1->current_frame_host()->GetProcess()->GetID());
679 ASSERT_EQ(child1_url, child1->current_url());
680 }
681
682 class SecurityExploitBrowserTestMojoBlobURLs
683 : public SecurityExploitBrowserTest {
684 public:
685 SecurityExploitBrowserTestMojoBlobURLs() = default;
686
TearDown()687 void TearDown() override {
688 storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(nullptr);
689 }
690 };
691
692 // Check that when site isolation is enabled, an origin can't create a blob URL
693 // for a different origin. Similar to the test above, but checks the
694 // mojo-based Blob URL implementation. See https://crbug.com/886976.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestMojoBlobURLs,CreateMojoBlobURLInDifferentOrigin)695 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestMojoBlobURLs,
696 CreateMojoBlobURLInDifferentOrigin) {
697 IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
698
699 GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
700 EXPECT_TRUE(NavigateToURL(shell(), main_url));
701 RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame();
702
703 // Intercept future blob URL registrations and overwrite the blob URL origin
704 // with b.com.
705 std::string target_origin = "http://b.com";
706 std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd";
707 auto intercept_hook =
708 base::BindRepeating(&BlobURLStoreInterceptor::Intercept,
709 GURL("blob:" + target_origin + "/" + blob_path));
710 storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook);
711
712 // Register a blob URL from the a.com main frame, which will go through the
713 // interceptor above and be rewritten to register the blob URL with the b.com
714 // origin. This should result in a kill because a.com should not be allowed
715 // to create blob URLs outside of its own origin.
716 content::RenderProcessHostBadMojoMessageWaiter crash_observer(
717 rfh->GetProcess());
718
719 // The renderer should always get killed, but sometimes ExecuteScript returns
720 // true anyway, so just ignore the result.
721 ignore_result(
722 content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))"));
723
724 // If the process is killed, this test passes.
725 EXPECT_EQ(
726 "Received bad user message: "
727 "Non committable URL passed to BlobURLStore::Register",
728 crash_observer.Wait());
729 }
730
731 // Check that with site isolation enabled, an origin can't create a filesystem
732 // URL for a different origin. See https://crbug.com/888001.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,CreateFilesystemURLInDifferentOrigin)733 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
734 CreateFilesystemURLInDifferentOrigin) {
735 IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
736
737 GURL main_url(embedded_test_server()->GetURL(
738 "a.com", "/cross_site_iframe_factory.html?a(b)"));
739 EXPECT_TRUE(NavigateToURL(shell(), main_url));
740 RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame();
741
742 // Block the renderer on operation that never completes, to shield it from
743 // receiving unexpected browser->renderer IPCs that might CHECK.
744 rfh->ExecuteJavaScriptWithUserGestureForTests(
745 base::ASCIIToUTF16("var r = new XMLHttpRequest();"
746 "r.open('GET', '/slow?99999', false);"
747 "r.send(null);"
748 "while (1);"));
749
750 // Set up a blob ID and populate it with attacker-controlled value. This
751 // is just using the blob APIs directly since creating arbitrary blobs is not
752 // what is prohibited; this data is not in any origin.
753 std::string payload = "<html><body>pwned.</body></html>";
754 std::string payload_type = "text/html";
755 std::unique_ptr<content::BlobHandle> blob = CreateMemoryBackedBlob(
756 rfh->GetSiteInstance()->GetBrowserContext(), payload, payload_type);
757 std::string blob_id = blob->GetUUID();
758
759 // Target a different origin.
760 std::string target_origin = "http://b.com";
761 GURL target_url =
762 GURL("filesystem:" + target_origin + "/temporary/exploit.html");
763
764 // Note: a well-behaved renderer would always call Open first before calling
765 // Create and Write, but it's actually not necessary for the original attack
766 // to succeed, so we omit it. As a result there are some log warnings from the
767 // quota observer.
768
769 PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, false,
770 false, false);
771
772 // Write the blob into the file. If successful, this places an
773 // attacker-controlled value in a resource on the target origin.
774 PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, blob_id,
775 0);
776
777 // Now navigate to |target_url| in a subframe. It should not succeed, and the
778 // subframe should not contain |payload|.
779 TestNavigationObserver observer(shell()->web_contents());
780 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
781 ->GetFrameTree()
782 ->root();
783 NavigateFrameToURL(root->child_at(0), target_url);
784 EXPECT_FALSE(observer.last_navigation_succeeded());
785 EXPECT_EQ(net::ERR_FILE_NOT_FOUND, observer.last_net_error_code());
786
787 RenderFrameHost* attacked_rfh = root->child_at(0)->current_frame_host();
788 std::string body =
789 EvalJs(attacked_rfh, "document.body.innerText").ExtractString();
790 EXPECT_TRUE(base::StartsWith(body, "Could not load the requested resource",
791 base::CompareCase::INSENSITIVE_ASCII))
792 << " body=" << body;
793 }
794
795 // Verify that when a compromised renderer tries to navigate a remote frame to
796 // a disallowed URL (e.g., file URL), that navigation is blocked.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,BlockIllegalOpenURLFromRemoteFrame)797 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
798 BlockIllegalOpenURLFromRemoteFrame) {
799 // Explicitly isolating a.com helps ensure that this test is applicable on
800 // platforms without site-per-process.
801 IsolateOrigin("a.com");
802
803 GURL main_url(embedded_test_server()->GetURL(
804 "a.com", "/cross_site_iframe_factory.html?a(b)"));
805 EXPECT_TRUE(NavigateToURL(shell(), main_url));
806 FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
807 ->GetFrameTree()
808 ->root();
809 FrameTreeNode* child = root->child_at(0);
810
811 // Simulate an IPC message where the top frame asks the remote subframe to
812 // navigate to a file: URL.
813 SiteInstance* a_com_instance = root->current_frame_host()->GetSiteInstance();
814 RenderFrameProxyHost* proxy =
815 child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
816 EXPECT_TRUE(proxy);
817
818 TestNavigationObserver observer(shell()->web_contents());
819 static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
820 ->OpenURL(CreateOpenURLParams(GURL("file:///")));
821 observer.Wait();
822
823 // Verify that the malicious navigation was blocked. Currently, this happens
824 // by rewriting the target URL to about:blank#blocked.
825 //
826 // TODO(alexmos): Consider killing the renderer process in this case, since
827 // this security check is already enforced in the renderer process.
828 EXPECT_EQ(GURL(kBlockedURL),
829 child->current_frame_host()->GetLastCommittedURL());
830
831 // Navigate to the starting page again to recreate the proxy, then try the
832 // same malicious navigation with a chrome:// URL.
833 EXPECT_TRUE(NavigateToURL(shell(), main_url));
834 child = root->child_at(0);
835 proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
836 EXPECT_TRUE(proxy);
837
838 TestNavigationObserver observer_2(shell()->web_contents());
839 GURL chrome_url(std::string(kChromeUIScheme) + "://" +
840 std::string(kChromeUIGpuHost));
841 static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
842 ->OpenURL(CreateOpenURLParams(chrome_url));
843 observer_2.Wait();
844 EXPECT_EQ(GURL(kBlockedURL),
845 child->current_frame_host()->GetLastCommittedURL());
846 }
847
848 class RouteMessageEventInterceptor
849 : public blink::mojom::RemoteFrameHostInterceptorForTesting {
850 public:
RouteMessageEventInterceptor(RenderFrameProxyHost * render_frame_proxy_host,const base::string16 & evil_origin)851 explicit RouteMessageEventInterceptor(
852 RenderFrameProxyHost* render_frame_proxy_host,
853 const base::string16& evil_origin)
854 : render_frame_proxy_host_(render_frame_proxy_host),
855 evil_origin_(evil_origin) {
856 render_frame_proxy_host_->frame_host_receiver_for_testing()
857 .SwapImplForTesting(this);
858 }
859
GetForwardingInterface()860 RemoteFrameHost* GetForwardingInterface() override {
861 return render_frame_proxy_host_;
862 }
863
RouteMessageEvent(const base::Optional<base::UnguessableToken> & source_frame_token,const base::string16 & source_origin,const base::string16 & target_origin,blink::TransferableMessage message)864 void RouteMessageEvent(
865 const base::Optional<base::UnguessableToken>& source_frame_token,
866 const base::string16& source_origin,
867 const base::string16& target_origin,
868 blink::TransferableMessage message) override {
869 // Forward the message to the actual RFPH replacing |source_origin| with the
870 // "evil origin" as especified in SetEvilSourceOriginAndWaitForMessage().
871 GetForwardingInterface()->RouteMessageEvent(
872 std::move(source_frame_token), std::move(evil_origin_),
873 std::move(target_origin), std::move(message));
874 }
875
876 private:
877 RenderFrameProxyHost* render_frame_proxy_host_;
878 base::string16 evil_origin_;
879 };
880
881 // Test verifying that a compromised renderer can't lie about the source_origin
882 // passed along with the RouteMessageEvent() mojo message. See also
883 // https://crbug.com/915721.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,PostMessageSourceOrigin)884 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageSourceOrigin) {
885 // Explicitly isolating a.com helps ensure that this test is applicable on
886 // platforms without site-per-process.
887 IsolateOrigin("b.com");
888
889 // Navigate to a page with an OOPIF.
890 GURL main_url(embedded_test_server()->GetURL(
891 "a.com", "/cross_site_iframe_factory.html?a(b)"));
892 EXPECT_TRUE(NavigateToURL(shell(), main_url));
893
894 // Sanity check of test setup: main frame and subframe should be isolated.
895 WebContents* web_contents = shell()->web_contents();
896 RenderFrameHost* main_frame = web_contents->GetMainFrame();
897 RenderFrameHost* subframe = web_contents->GetAllFrames()[1];
898 EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());
899
900 // We need to get ahold of the RenderFrameProxyHost representing the main
901 // frame for the subframe's process, to install the mojo interceptor.
902 FrameTreeNode* main_frame_node =
903 static_cast<WebContentsImpl*>(shell()->web_contents())
904 ->GetFrameTree()
905 ->root();
906 FrameTreeNode* subframe_node = main_frame_node->child_at(0);
907 SiteInstance* b_com_instance =
908 subframe_node->current_frame_host()->GetSiteInstance();
909 RenderFrameProxyHost* main_frame_proxy_host =
910 main_frame_node->render_manager()->GetRenderFrameProxyHost(
911 b_com_instance);
912
913 // Prepare to intercept the RouteMessageEvent IPC message that will come
914 // from the subframe process.
915 url::Origin invalid_origin =
916 web_contents->GetMainFrame()->GetLastCommittedOrigin();
917 base::string16 evil_source_origin =
918 base::UTF8ToUTF16(invalid_origin.Serialize());
919 RouteMessageEventInterceptor mojo_interceptor(main_frame_proxy_host,
920 evil_source_origin);
921
922 // Post a message from the subframe to the cross-site parent and intercept the
923 // associated IPC message, changing it to simulate a compromised subframe
924 // renderer lying that the |source_origin| of the postMessage is the origin of
925 // the parent (not of the subframe).
926 RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
927 EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')"));
928 EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN,
929 kill_waiter.Wait());
930 }
931
932 // Intercepts calls to RenderFramHost's OpenURL mojo method, and
933 // store the passed parameter.
934 class OpenURLInterceptor
935 : public mojom::RenderFrameProxyHostInterceptorForTesting {
936 public:
OpenURLInterceptor(content::RenderFrameProxyHost * render_frame_proxy_host)937 explicit OpenURLInterceptor(
938 content::RenderFrameProxyHost* render_frame_proxy_host)
939 : render_frame_proxy_host_(render_frame_proxy_host),
940 intercepted_params_(mojom::OpenURLParams::New()) {
941 render_frame_proxy_host_->frame_proxy_host_receiver_for_testing()
942 .SwapImplForTesting(this);
943 }
944
945 ~OpenURLInterceptor() override = default;
946
GetForwardingInterface()947 mojom::RenderFrameProxyHost* GetForwardingInterface() override {
948 return render_frame_proxy_host_;
949 }
950
OpenURL(mojom::OpenURLParamsPtr params)951 void OpenURL(mojom::OpenURLParamsPtr params) override {
952 intercepted_params_ = std::move(params);
953 }
954
GetInterceptedParams()955 mojom::OpenURLParamsPtr GetInterceptedParams() {
956 return std::move(intercepted_params_);
957 }
958
959 private:
960 content::RenderFrameProxyHost* render_frame_proxy_host_;
961 mojom::OpenURLParamsPtr intercepted_params_;
962
963 DISALLOW_COPY_AND_ASSIGN(OpenURLInterceptor);
964 };
965
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,InvalidRemoteNavigationInitiator)966 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
967 InvalidRemoteNavigationInitiator) {
968 // Explicitly isolating a.com helps ensure that this test is applicable on
969 // platforms without site-per-process.
970 IsolateOrigin("a.com");
971
972 // Navigate to a test page where the subframe is cross-site (and because of
973 // IsolateOrigin call above in a separate process) from the main frame.
974 GURL main_url(embedded_test_server()->GetURL(
975 "a.com", "/cross_site_iframe_factory.html?a(b)"));
976 EXPECT_TRUE(NavigateToURL(shell(), main_url));
977 RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
978 shell()->web_contents()->GetMainFrame());
979 RenderProcessHost* main_process = main_frame->GetProcess();
980 EXPECT_EQ(2u, shell()->web_contents()->GetAllFrames().size());
981 RenderFrameHost* subframe = shell()->web_contents()->GetAllFrames()[1];
982 RenderProcessHost* subframe_process = subframe->GetProcess();
983 EXPECT_NE(main_process->GetID(), subframe_process->GetID());
984
985 // Prepare to intercept OpenURL Mojo message that will come from
986 // the main frame.
987 FrameTreeNode* main_frame_node =
988 static_cast<WebContentsImpl*>(shell()->web_contents())
989 ->GetFrameTree()
990 ->root();
991 FrameTreeNode* child_node = main_frame_node->child_at(0);
992 SiteInstance* a_com_instance =
993 main_frame_node->current_frame_host()->GetSiteInstance();
994 RenderFrameProxyHost* proxy =
995 child_node->render_manager()->GetRenderFrameProxyHost(a_com_instance);
996
997 OpenURLInterceptor interceptor(proxy);
998
999 // Have the main frame request navigation in the "remote" subframe. This will
1000 // result in OpenURL Mojo message being sent to the RenderFrameProxyHost.
1001 EXPECT_TRUE(ExecJs(shell()->web_contents()->GetMainFrame(),
1002 "window.frames[0].location = '/title1.html';"));
1003
1004 // Change the intercepted message to simulate a compromised subframe renderer
1005 // lying that the |initiator_origin| is the origin of the |subframe|.
1006 auto evil_params = interceptor.GetInterceptedParams();
1007 evil_params->initiator_origin = subframe->GetLastCommittedOrigin();
1008
1009 // Inject the invalid IPC and verify that the renderer gets terminated.
1010 RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
1011 static_cast<mojom::FrameHost*>(main_frame)->OpenURL(std::move(evil_params));
1012 EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
1013 }
1014
1015 class BeginNavigationInitiatorReplacer : public FrameHostInterceptor {
1016 public:
BeginNavigationInitiatorReplacer(WebContents * web_contents,base::Optional<url::Origin> initiator_to_inject)1017 BeginNavigationInitiatorReplacer(
1018 WebContents* web_contents,
1019 base::Optional<url::Origin> initiator_to_inject)
1020 : FrameHostInterceptor(web_contents),
1021 initiator_to_inject_(initiator_to_inject) {}
1022
WillDispatchBeginNavigation(RenderFrameHost * render_frame_host,mojom::CommonNavigationParamsPtr * common_params,mojom::BeginNavigationParamsPtr * begin_params,mojo::PendingRemote<blink::mojom::BlobURLToken> * blob_url_token,mojo::PendingAssociatedRemote<mojom::NavigationClient> * navigation_client,mojo::PendingRemote<blink::mojom::NavigationInitiator> * navigation_initiator)1023 bool WillDispatchBeginNavigation(
1024 RenderFrameHost* render_frame_host,
1025 mojom::CommonNavigationParamsPtr* common_params,
1026 mojom::BeginNavigationParamsPtr* begin_params,
1027 mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
1028 mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client,
1029 mojo::PendingRemote<blink::mojom::NavigationInitiator>*
1030 navigation_initiator) override {
1031 if (is_activated_) {
1032 (*common_params)->initiator_origin = initiator_to_inject_;
1033 is_activated_ = false;
1034 }
1035
1036 return true;
1037 }
1038
Activate()1039 void Activate() { is_activated_ = true; }
1040
1041 private:
1042 base::Optional<url::Origin> initiator_to_inject_;
1043 bool is_activated_ = false;
1044
1045 DISALLOW_COPY_AND_ASSIGN(BeginNavigationInitiatorReplacer);
1046 };
1047
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,InvalidBeginNavigationInitiator)1048 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
1049 InvalidBeginNavigationInitiator) {
1050 WebContentsImpl* web_contents =
1051 static_cast<WebContentsImpl*>(shell()->web_contents());
1052
1053 // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
1054 // the test creates the RenderFrameHostImpl that is the target of the IPC.
1055 BeginNavigationInitiatorReplacer injector(
1056 web_contents, url::Origin::Create(GURL("http://b.com")));
1057
1058 // Explicitly isolating a.com helps ensure that this test is applicable on
1059 // platforms without site-per-process.
1060 IsolateOrigin("a.com");
1061
1062 // Navigate to a test page that will be locked to a.com.
1063 GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
1064 EXPECT_TRUE(NavigateToURL(web_contents, main_url));
1065
1066 // Start monitoring for renderer kills.
1067 RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess();
1068 RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
1069
1070 // Have the main frame navigate and lie that the initiator origin is b.com.
1071 injector.Activate();
1072 // Don't expect a response for the script, as the process may be killed
1073 // before the script sends its completion message.
1074 ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
1075
1076 // Verify that the renderer was terminated.
1077 EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
1078 }
1079
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,MissingBeginNavigationInitiator)1080 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
1081 MissingBeginNavigationInitiator) {
1082 // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
1083 // the test creates the RenderFrameHostImpl that is the target of the IPC.
1084 WebContents* web_contents = shell()->web_contents();
1085 BeginNavigationInitiatorReplacer injector(web_contents, base::nullopt);
1086
1087 // Navigate to a test page.
1088 GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
1089 EXPECT_TRUE(NavigateToURL(web_contents, main_url));
1090
1091 // Start monitoring for renderer kills.
1092 RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess();
1093 RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
1094
1095 // Have the main frame submit a BeginNavigation IPC with a missing initiator.
1096 injector.Activate();
1097 // Don't expect a response for the script, as the process may be killed
1098 // before the script sends its completion message.
1099 ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
1100
1101 // Verify that the renderer was terminated.
1102 EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN,
1103 kill_waiter.Wait());
1104 }
1105
1106 namespace {
1107
1108 // An interceptor class that allows replacing the URL of the commit IPC from
1109 // the renderer process to the browser process.
1110 class DidCommitUrlReplacer : public DidCommitNavigationInterceptor {
1111 public:
DidCommitUrlReplacer(WebContents * web_contents,const GURL & replacement_url)1112 DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url)
1113 : DidCommitNavigationInterceptor(web_contents),
1114 replacement_url_(replacement_url) {}
1115 ~DidCommitUrlReplacer() override = default;
1116
1117 protected:
WillProcessDidCommitNavigation(RenderFrameHost * render_frame_host,NavigationRequest * navigation_request,::FrameHostMsg_DidCommitProvisionalLoad_Params * params,mojom::DidCommitProvisionalLoadInterfaceParamsPtr * interface_params)1118 bool WillProcessDidCommitNavigation(
1119 RenderFrameHost* render_frame_host,
1120 NavigationRequest* navigation_request,
1121 ::FrameHostMsg_DidCommitProvisionalLoad_Params* params,
1122 mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
1123 override {
1124 params->url = replacement_url_;
1125 return true;
1126 }
1127
1128 private:
1129 GURL replacement_url_;
1130
1131 DISALLOW_COPY_AND_ASSIGN(DidCommitUrlReplacer);
1132 };
1133
1134 } // namespace
1135
1136 // Test which verifies that when an exploited renderer process sends a commit
1137 // message with URL that the process is not allowed to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,DidCommitInvalidURL)1138 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURL) {
1139 // Explicitly isolating foo.com helps ensure that this test is applicable on
1140 // platforms without site-per-process.
1141 IsolateOrigin("foo.com");
1142
1143 RenderFrameDeletedObserver initial_frame_deleted_observer(
1144 shell()->web_contents()->GetMainFrame());
1145
1146 // Test assumes the initial RenderFrameHost to be deleted. Disable
1147 // back-forward cache to ensure that it doesn't get preserved in the cache.
1148 DisableBackForwardCacheForTesting(shell()->web_contents(),
1149 BackForwardCache::TEST_ASSUMES_NO_CACHING);
1150
1151 // Navigate to foo.com initially.
1152 GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
1153 EXPECT_TRUE(NavigateToURL(shell(), foo_url));
1154
1155 // Wait for the RenderFrameHost which was current before the navigation to
1156 // foo.com to be deleted. This is necessary, since on a slow system the
1157 // UnloadACK event can arrive after the DidCommitUrlReplacer instance below
1158 // is created. The replacer code has checks to ensure that all frames being
1159 // deleted it has seen being created, which with delayed UnloadACK is
1160 // violated.
1161 initial_frame_deleted_observer.WaitUntilDeleted();
1162
1163 // Create the interceptor object which will replace the URL of the subsequent
1164 // navigation with bar.com based URL.
1165 GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
1166 DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url);
1167
1168 // Navigate to another URL within foo.com, which would usually be committed
1169 // successfully, but when the URL is modified it should result in the
1170 // termination of the renderer process.
1171 RenderProcessHostBadIpcMessageWaiter kill_waiter(
1172 shell()->web_contents()->GetMainFrame()->GetProcess());
1173 EXPECT_FALSE(NavigateToURL(
1174 shell(), embedded_test_server()->GetURL("foo.com", "/title2.html")));
1175 EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
1176 }
1177
1178 // Test which verifies that when an exploited renderer process sends a commit
1179 // message with URL that the process is not allowed to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,DidCommitInvalidURLWithOpaqueOrigin)1180 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
1181 DidCommitInvalidURLWithOpaqueOrigin) {
1182 // Explicitly isolating foo.com helps ensure that this test is applicable on
1183 // platforms without site-per-process.
1184 IsolateOrigin("foo.com");
1185
1186 RenderFrameDeletedObserver initial_frame_deleted_observer(
1187 shell()->web_contents()->GetMainFrame());
1188
1189 // Test assumes the initial RenderFrameHost to be deleted. Disable
1190 // back-forward cache to ensure that it doesn't get preserved in the cache.
1191 DisableBackForwardCacheForTesting(shell()->web_contents(),
1192 BackForwardCache::TEST_ASSUMES_NO_CACHING);
1193
1194 // Navigate to foo.com initially.
1195 GURL foo_url(embedded_test_server()->GetURL("foo.com",
1196 "/page_with_blank_iframe.html"));
1197 EXPECT_TRUE(NavigateToURL(shell(), foo_url));
1198
1199 // Wait for the RenderFrameHost which was current before the navigation to
1200 // foo.com to be deleted. This is necessary, since on a slow system the
1201 // UnloadACK event can arrive after the DidCommitUrlReplacer instance below
1202 // is created. The replacer code has checks to ensure that all frames being
1203 // deleted it has seen being created, which with delayed UnloadACK is
1204 // violated.
1205 initial_frame_deleted_observer.WaitUntilDeleted();
1206
1207 // Create the interceptor object which will replace the URL of the subsequent
1208 // navigation with bar.com based URL.
1209 GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
1210 DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url);
1211
1212 // Navigate the subframe to a data URL, which would usually be committed
1213 // successfully in the same process as foo.com, but when the URL is modified
1214 // it should result in the termination of the renderer process.
1215 RenderProcessHostBadIpcMessageWaiter kill_waiter(
1216 shell()->web_contents()->GetMainFrame()->GetProcess());
1217 GURL data_url(R"(data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E)");
1218 EXPECT_TRUE(
1219 NavigateIframeToURL(shell()->web_contents(), "test_iframe", data_url));
1220 EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
1221 }
1222
1223 // Test which verifies that a WebUI process cannot send a commit message with
1224 // URL for a web document.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,WebUIProcessDidCommitWebURL)1225 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
1226 WebUIProcessDidCommitWebURL) {
1227 // Navigate to a WebUI document.
1228 GURL webui_url(GetWebUIURL(kChromeUIGpuHost));
1229 EXPECT_TRUE(NavigateToURL(shell(), webui_url));
1230
1231 // Create the interceptor object which will replace the URL of the subsequent
1232 // navigation with |web_url|.
1233 GURL web_url(embedded_test_server()->GetURL("foo.com", "/title3.html"));
1234 DidCommitUrlReplacer url_replacer(shell()->web_contents(), web_url);
1235
1236 // Navigate to another URL within the WebUI, which would usually be committed
1237 // successfully, but when the URL is modified it should result in the
1238 // termination of the renderer process.
1239 RenderProcessHostBadIpcMessageWaiter kill_waiter(
1240 shell()->web_contents()->GetMainFrame()->GetProcess());
1241 GURL second_webui_url(webui_url.Resolve("/foo"));
1242 EXPECT_FALSE(NavigateToURL(shell(), second_webui_url));
1243 EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
1244 }
1245
1246 // Test that verifies that if a RenderFrameHost is incorrectly given WebUI
1247 // bindings, committing a non-WebUI URL in it is detected and the process is
1248 // correctly terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,DidCommitNonWebUIURLInProcessWithBindings)1249 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
1250 DidCommitNonWebUIURLInProcessWithBindings) {
1251 // Navigate to a web URL.
1252 GURL initial_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
1253 EXPECT_TRUE(NavigateToURL(shell(), initial_url));
1254
1255 // Start a second navigation.
1256 GURL web_url(embedded_test_server()->GetURL("foo.com", "/title2.html"));
1257 TestNavigationManager navigation(shell()->web_contents(), web_url);
1258 RenderProcessHostBadIpcMessageWaiter kill_waiter(
1259 shell()->web_contents()->GetMainFrame()->GetProcess());
1260
1261 shell()->LoadURL(web_url);
1262 EXPECT_TRUE(navigation.WaitForResponse());
1263
1264 // Grant WebUI bindings to the navigated frame to simulate a bug in the code
1265 // that incorrectly does it for a navigation that does not require it.
1266 navigation.GetNavigationHandle()->GetRenderFrameHost()->AllowBindings(
1267 BINDINGS_POLICY_WEB_UI);
1268
1269 // Resume the navigation and upon receiving the commit message the renderer
1270 // process will be terminated.
1271 navigation.ResumeNavigation();
1272 EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
1273 }
1274
1275 class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
1276 public:
BeginNavigationTransitionReplacer(WebContents * web_contents,ui::PageTransition transition_to_inject)1277 BeginNavigationTransitionReplacer(WebContents* web_contents,
1278 ui::PageTransition transition_to_inject)
1279 : FrameHostInterceptor(web_contents),
1280 transition_to_inject_(transition_to_inject) {}
1281
WillDispatchBeginNavigation(RenderFrameHost * render_frame_host,mojom::CommonNavigationParamsPtr * common_params,mojom::BeginNavigationParamsPtr * begin_params,mojo::PendingRemote<blink::mojom::BlobURLToken> * blob_url_token,mojo::PendingAssociatedRemote<mojom::NavigationClient> * navigation_client,mojo::PendingRemote<blink::mojom::NavigationInitiator> * navigation_initiator)1282 bool WillDispatchBeginNavigation(
1283 RenderFrameHost* render_frame_host,
1284 mojom::CommonNavigationParamsPtr* common_params,
1285 mojom::BeginNavigationParamsPtr* begin_params,
1286 mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
1287 mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client,
1288 mojo::PendingRemote<blink::mojom::NavigationInitiator>*
1289 navigation_initiator) override {
1290 if (is_activated_) {
1291 (*common_params)->transition = transition_to_inject_;
1292 is_activated_ = false;
1293 }
1294
1295 return true;
1296 }
1297
Activate()1298 void Activate() { is_activated_ = true; }
1299
1300 private:
1301 ui::PageTransition transition_to_inject_;
1302 bool is_activated_ = false;
1303
1304 DISALLOW_COPY_AND_ASSIGN(BeginNavigationTransitionReplacer);
1305 };
1306
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,NonWebbyTransition)1307 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonWebbyTransition) {
1308 const ui::PageTransition test_cases[] = {
1309 ui::PAGE_TRANSITION_TYPED,
1310 ui::PAGE_TRANSITION_AUTO_BOOKMARK,
1311 ui::PAGE_TRANSITION_GENERATED,
1312 ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
1313 ui::PAGE_TRANSITION_RELOAD,
1314 ui::PAGE_TRANSITION_KEYWORD,
1315 ui::PAGE_TRANSITION_KEYWORD_GENERATED};
1316
1317 for (ui::PageTransition transition : test_cases) {
1318 // Prepare to intercept BeginNavigation mojo IPC. This has to be done
1319 // before the test creates the RenderFrameHostImpl that is the target of the
1320 // IPC.
1321 WebContents* web_contents = shell()->web_contents();
1322 BeginNavigationTransitionReplacer injector(web_contents, transition);
1323
1324 // Navigate to a test page.
1325 GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
1326 EXPECT_TRUE(NavigateToURL(web_contents, main_url));
1327
1328 // Start monitoring for renderer kills.
1329 RenderProcessHost* main_process =
1330 web_contents->GetMainFrame()->GetProcess();
1331 RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
1332
1333 // Have the main frame submit a BeginNavigation IPC with a missing
1334 // initiator.
1335 injector.Activate();
1336 // Don't expect a response for the script, as the process may be killed
1337 // before the script sends its completion message.
1338 ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
1339
1340 // Verify that the renderer was terminated.
1341 EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION,
1342 kill_waiter.Wait());
1343 }
1344 }
1345
1346 class SecurityExploitViaDisabledWebSecurityTest
1347 : public SecurityExploitBrowserTest {
1348 public:
SecurityExploitViaDisabledWebSecurityTest()1349 SecurityExploitViaDisabledWebSecurityTest() {
1350 // To get around BlockedSchemeNavigationThrottle. Other attempts at getting
1351 // around it don't work, i.e.:
1352 // -if the request is made in a child frame then the frame is torn down
1353 // immediately on process killing so the navigation doesn't complete
1354 // -if it's classified as same document, then a DCHECK in
1355 // NavigationRequest::CreateRendererInitiated fires
1356 feature_list_.InitAndEnableFeature(
1357 features::kAllowContentInitiatedDataUrlNavigations);
1358 }
1359
1360 protected:
SetUpCommandLine(base::CommandLine * command_line)1361 void SetUpCommandLine(base::CommandLine* command_line) override {
1362 // Simulate a compromised renderer, otherwise the cross-origin request to
1363 // file: is blocked.
1364 command_line->AppendSwitch(switches::kDisableWebSecurity);
1365 SecurityExploitBrowserTest::SetUpCommandLine(command_line);
1366 }
1367
1368 private:
1369 base::test::ScopedFeatureList feature_list_;
1370 };
1371
1372 // Test to verify that an exploited renderer process trying to specify a
1373 // non-empty URL for base_url_for_data_url on navigation is correctly
1374 // terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,ValidateBaseUrlForDataUrl)1375 IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
1376 ValidateBaseUrlForDataUrl) {
1377 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
1378 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1379
1380 RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
1381 shell()->web_contents()->GetMainFrame());
1382
1383 GURL data_url("data:text/html,foo");
1384 base::FilePath file_path = GetTestFilePath("", "simple_page.html");
1385 GURL file_url = net::FilePathToFileURL(file_path);
1386
1387 // Setup a BeginNavigate IPC with non-empty base_url_for_data_url.
1388 mojom::CommonNavigationParamsPtr common_params =
1389 mojom::CommonNavigationParams::New(
1390 data_url, url::Origin::Create(data_url),
1391 blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK,
1392 mojom::NavigationType::DIFFERENT_DOCUMENT, NavigationDownloadPolicy(),
1393 false /* should_replace_current_entry */,
1394 file_url, /* base_url_for_data_url */
1395 GURL() /* history_url_for_data_url */,
1396 blink::PreviewsTypes::PREVIEWS_UNSPECIFIED,
1397 base::TimeTicks::Now() /* navigation_start */, "GET",
1398 nullptr /* post_data */, network::mojom::SourceLocation::New(),
1399 false /* started_from_context_menu */, false /* has_user_gesture */,
1400 false /* text_fragment_token */, CreateInitiatorCSPInfo(),
1401 std::vector<int>() /* initiator_origin_trial_features */,
1402 std::string() /* href_translate */,
1403 false /* is_history_navigation_in_new_child_frame */,
1404 base::TimeTicks() /* input_start */);
1405 mojom::BeginNavigationParamsPtr begin_params =
1406 mojom::BeginNavigationParams::New(
1407 MSG_ROUTING_NONE /* initiator_routing_id */,
1408 std::string() /* headers */, net::LOAD_NORMAL,
1409 false /* skip_service_worker */,
1410 blink::mojom::RequestContextType::LOCATION,
1411 network::mojom::RequestDestination::kDocument,
1412 blink::WebMixedContentContextType::kBlockable,
1413 false /* is_form_submission */,
1414 false /* was_initiated_by_link_click */,
1415 GURL() /* searchable_form_url */,
1416 std::string() /* searchable_form_encoding */,
1417 GURL() /* client_side_redirect_url */,
1418 base::nullopt /* devtools_initiator_info */,
1419 false /* force_ignore_site_for_cookies */,
1420 nullptr /* trust_token_params */, base::nullopt /* impression */,
1421 base::TimeTicks() /* renderer_before_unload_start */,
1422 base::TimeTicks() /* renderer_before_unload_end */);
1423
1424 // Receiving the invalid IPC message should lead to renderer process
1425 // termination.
1426 RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess());
1427
1428 mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
1429 auto navigation_client_receiver =
1430 navigation_client.InitWithNewEndpointAndPassReceiver();
1431 rfh->frame_host_receiver_for_testing().impl()->BeginNavigation(
1432 std::move(common_params), std::move(begin_params), mojo::NullRemote(),
1433 std::move(navigation_client), mojo::NullRemote());
1434 EXPECT_EQ(bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED,
1435 process_kill_waiter.Wait());
1436
1437 EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
1438 rfh->GetProcess()->GetID(), file_path));
1439
1440 // Reload the page to create another renderer process.
1441 TestNavigationObserver tab_observer(shell()->web_contents(), 1);
1442 shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
1443 tab_observer.Wait();
1444
1445 // Make an XHR request to check if the page has access.
1446 std::string script = base::StringPrintf(
1447 "var xhr = new XMLHttpRequest()\n"
1448 "xhr.open('GET', '%s', false);\n"
1449 "try { xhr.send(); } catch (e) {}\n"
1450 "window.domAutomationController.send(xhr.responseText);",
1451 file_url.spec().c_str());
1452 std::string result;
1453 EXPECT_TRUE(
1454 ExecuteScriptAndExtractString(shell()->web_contents(), script, &result));
1455 EXPECT_TRUE(result.empty());
1456 }
1457
1458 // Tests what happens when a web renderer asks to begin navigating to a file
1459 // url.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,WebToFileNavigation)1460 IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
1461 WebToFileNavigation) {
1462 // Navigate to a web page.
1463 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
1464 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1465
1466 // Have the webpage attempt to open a window with a file URL.
1467 //
1468 // Note that such attempt would normally be blocked in the renderer ("Not
1469 // allowed to load local resource: file:///..."), but the test here simulates
1470 // a compromised renderer by using --disable-web-security cmdline flag.
1471 GURL file_url = GetTestUrl("", "simple_page.html");
1472 WebContentsAddedObserver new_window_observer;
1473 TestNavigationObserver nav_observer(nullptr);
1474 nav_observer.StartWatchingNewWebContents();
1475 ASSERT_TRUE(ExecJs(shell()->web_contents(),
1476 JsReplace("window.open($1, '_blank')", file_url)));
1477 WebContents* new_window = new_window_observer.GetWebContents();
1478 nav_observer.WaitForNavigationFinished();
1479
1480 // Verify that the navigation got blocked.
1481 EXPECT_TRUE(nav_observer.last_navigation_succeeded());
1482 EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url());
1483 EXPECT_EQ(GURL(kBlockedURL),
1484 new_window->GetMainFrame()->GetLastCommittedURL());
1485 EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(),
1486 new_window->GetMainFrame()->GetLastCommittedOrigin());
1487 EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(),
1488 new_window->GetMainFrame()->GetProcess());
1489
1490 // Even though the navigation is blocked, we expect the opener relationship to
1491 // be established between the 2 windows.
1492 EXPECT_EQ(true, ExecJs(new_window, "!!window.opener"));
1493 }
1494
1495 // Tests what happens when a web renderer asks to begin navigating to a
1496 // view-source url.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,WebToViewSourceNavigation)1497 IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
1498 WebToViewSourceNavigation) {
1499 // Navigate to a web page.
1500 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
1501 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1502
1503 // Have the webpage attempt to open a window with a view-source URL.
1504 //
1505 // Note that such attempt would normally be blocked in the renderer ("Not
1506 // allowed to load local resource: view-source:///..."), but the test here
1507 // simulates a compromised renderer by using --disable-web-security flag.
1508 base::FilePath file_path = GetTestFilePath("", "simple_page.html");
1509 GURL view_source_url =
1510 GURL(std::string(kViewSourceScheme) + ":" + start_url.spec());
1511 WebContentsAddedObserver new_window_observer;
1512 TestNavigationObserver nav_observer(nullptr);
1513 nav_observer.StartWatchingNewWebContents();
1514 ASSERT_TRUE(ExecJs(shell()->web_contents(),
1515 JsReplace("window.open($1, '_blank')", view_source_url)));
1516 WebContents* new_window = new_window_observer.GetWebContents();
1517 nav_observer.WaitForNavigationFinished();
1518
1519 // Verify that the navigation got blocked.
1520 EXPECT_TRUE(nav_observer.last_navigation_succeeded());
1521 EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url());
1522 EXPECT_EQ(GURL(kBlockedURL),
1523 new_window->GetMainFrame()->GetLastCommittedURL());
1524 EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(),
1525 new_window->GetMainFrame()->GetLastCommittedOrigin());
1526 EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(),
1527 new_window->GetMainFrame()->GetProcess());
1528
1529 // Even though the navigation is blocked, we expect the opener relationship to
1530 // be established between the 2 windows.
1531 EXPECT_EQ(true, ExecJs(new_window, "!!window.opener"));
1532 }
1533
1534 class BeginNavigationTrustTokenParamsReplacer : public FrameHostInterceptor {
1535 public:
BeginNavigationTrustTokenParamsReplacer(WebContents * web_contents,network::mojom::TrustTokenParamsPtr params_to_inject)1536 BeginNavigationTrustTokenParamsReplacer(
1537 WebContents* web_contents,
1538 network::mojom::TrustTokenParamsPtr params_to_inject)
1539 : FrameHostInterceptor(web_contents),
1540 params_to_inject_(std::move(params_to_inject)) {}
1541
1542 BeginNavigationTrustTokenParamsReplacer(
1543 const BeginNavigationTrustTokenParamsReplacer&) = delete;
1544 BeginNavigationTrustTokenParamsReplacer& operator=(
1545 const BeginNavigationTrustTokenParamsReplacer&) = delete;
1546
WillDispatchBeginNavigation(RenderFrameHost * render_frame_host,mojom::CommonNavigationParamsPtr * common_params,mojom::BeginNavigationParamsPtr * begin_params,mojo::PendingRemote<blink::mojom::BlobURLToken> * blob_url_token,mojo::PendingAssociatedRemote<mojom::NavigationClient> * navigation_client,mojo::PendingRemote<blink::mojom::NavigationInitiator> * navigation_initiator)1547 bool WillDispatchBeginNavigation(
1548 RenderFrameHost* render_frame_host,
1549 mojom::CommonNavigationParamsPtr* common_params,
1550 mojom::BeginNavigationParamsPtr* begin_params,
1551 mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
1552 mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client,
1553 mojo::PendingRemote<blink::mojom::NavigationInitiator>*
1554 navigation_initiator) override {
1555 if (is_activated_) {
1556 (*begin_params)->trust_token_params = params_to_inject_.Clone();
1557 is_activated_ = false;
1558 }
1559
1560 return true;
1561 }
1562
Activate()1563 void Activate() { is_activated_ = true; }
1564
1565 private:
1566 network::mojom::TrustTokenParamsPtr params_to_inject_;
1567 bool is_activated_ = false;
1568 };
1569
1570 class SecurityExploitBrowserTestWithTrustTokensEnabled
1571 : public SecurityExploitBrowserTest {
1572 public:
SecurityExploitBrowserTestWithTrustTokensEnabled()1573 SecurityExploitBrowserTestWithTrustTokensEnabled() {
1574 feature_list_.InitAndEnableFeature(network::features::kTrustTokens);
1575 }
1576
1577 private:
1578 base::test::ScopedFeatureList feature_list_;
1579 };
1580
1581 // Test that the browser correctly reports a bad message when a child frame
1582 // attempts to navigate with a Trust Tokens redemption operation associated with
1583 // the navigation, but its parent lacks the trust-token-redemption Feature
1584 // Policy feature.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,BrowserForbidsTrustTokenRedemptionWithoutFeaturePolicy)1585 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,
1586 BrowserForbidsTrustTokenRedemptionWithoutFeaturePolicy) {
1587 WebContents* web_contents = shell()->web_contents();
1588
1589 // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
1590 // the test creates the RenderFrameHostImpl that is the target of the IPC.
1591 auto params = network::mojom::TrustTokenParams::New();
1592 params->type = network::mojom::TrustTokenOperationType::kRedemption;
1593 BeginNavigationTrustTokenParamsReplacer replacer(web_contents,
1594 std::move(params));
1595
1596 GURL start_url(embedded_test_server()->GetURL(
1597 "/page-with-trust-token-feature-policy-disabled.html"));
1598 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1599
1600 RenderFrameHost* parent = web_contents->GetMainFrame();
1601 ASSERT_FALSE(parent->IsFeatureEnabled(
1602 blink::mojom::FeaturePolicyFeature::kTrustTokenRedemption));
1603
1604 RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
1605 ->GetFrameTree()
1606 ->root()
1607 ->child_at(0)
1608 ->current_frame_host();
1609 RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
1610
1611 replacer.Activate();
1612
1613 // Note: this can't use NavigateFrameToURL, because that method doesn't
1614 // route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL,
1615 // because that navigation will hang.
1616 //
1617 // It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will
1618 // finish before the renderer gets killed, and sometimes it won't.
1619 ignore_result(
1620 ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html"))));
1621
1622 EXPECT_THAT(kill_waiter.Wait(),
1623 Optional(HasSubstr("Feature Policy feature is absent")));
1624 }
1625
1626 // Test that the browser correctly reports a bad message when a child frame
1627 // attempts to navigate with a Trust Tokens signing operation associated with
1628 // the navigation, but its parent lacks the trust-token-redemption (sic) Feature
1629 // Policy feature.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,BrowserForbidsTrustTokenSigningWithoutFeaturePolicy)1630 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,
1631 BrowserForbidsTrustTokenSigningWithoutFeaturePolicy) {
1632 WebContents* web_contents = shell()->web_contents();
1633
1634 // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
1635 // the test creates the RenderFrameHostImpl that is the target of the IPC.
1636 auto params = network::mojom::TrustTokenParams::New();
1637 params->type = network::mojom::TrustTokenOperationType::kSigning;
1638 BeginNavigationTrustTokenParamsReplacer replacer(web_contents,
1639 std::move(params));
1640
1641 GURL start_url(embedded_test_server()->GetURL(
1642 "/page-with-trust-token-feature-policy-disabled.html"));
1643 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1644
1645 RenderFrameHost* parent = web_contents->GetMainFrame();
1646 ASSERT_FALSE(parent->IsFeatureEnabled(
1647 blink::mojom::FeaturePolicyFeature::kTrustTokenRedemption));
1648
1649 RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
1650 ->GetFrameTree()
1651 ->root()
1652 ->child_at(0)
1653 ->current_frame_host();
1654 RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
1655
1656 replacer.Activate();
1657
1658 // Note: this can't use NavigateFrameToURL, because that method doesn't
1659 // route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL,
1660 // because that navigation will hang.
1661 //
1662 // It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will
1663 // finish before the renderer gets killed, and sometimes it won't.
1664 ignore_result(
1665 ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html"))));
1666
1667 EXPECT_THAT(kill_waiter.Wait(),
1668 Optional(HasSubstr("Feature Policy feature is absent")));
1669 }
1670
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,BrowserForbidsTrustTokenParamsOnMainFrameNav)1671 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,
1672 BrowserForbidsTrustTokenParamsOnMainFrameNav) {
1673 WebContents* web_contents = shell()->web_contents();
1674
1675 // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
1676 // the test creates the RenderFrameHostImpl that is the target of the IPC.
1677 BeginNavigationTrustTokenParamsReplacer replacer(
1678 web_contents, network::mojom::TrustTokenParams::New());
1679
1680 GURL start_url(embedded_test_server()->GetURL("/title1.html"));
1681 EXPECT_TRUE(NavigateToURL(shell(), start_url));
1682
1683 RenderFrameHost* compromised_renderer = web_contents->GetMainFrame();
1684 RenderProcessHostBadMojoMessageWaiter kill_waiter(
1685 compromised_renderer->GetProcess());
1686
1687 replacer.Activate();
1688
1689 // Can't use NavigateToURL here because it would hang. Additionally, we can't
1690 // EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will finish
1691 // before the renderer gets killed, and sometimes it won't.
1692 ignore_result(ExecJs(compromised_renderer,
1693 JsReplace("location.href=$1", GURL("/title2.html"))));
1694
1695 EXPECT_THAT(kill_waiter.Wait(),
1696 Optional(HasSubstr("Trust Token params in main frame nav")));
1697 }
1698
1699 } // namespace content
1700