[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 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 | |
Sebastien Marchand | f1349f5 | 2019-01-25 03:16:41 | [diff] [blame] | 5 | #include "base/bind.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 6 | #include "base/command_line.h" |
avi | e4d7b6f | 2015-12-26 00:59:18 | [diff] [blame] | 7 | #include "base/macros.h" |
Gabriel Charette | 1b7d9303 | 2020-03-05 20:09:13 | [diff] [blame] | 8 | #include "base/memory/ptr_util.h" |
Gabriel Charette | b164afef | 2017-11-21 20:59:31 | [diff] [blame] | 9 | #include "base/run_loop.h" |
[email protected] | 135cb80 | 2013-06-09 16:44:20 | [diff] [blame] | 10 | #include "base/strings/utf_string_conversions.h" |
Devlin Cronin | 626d80c | 2018-06-01 01:08:36 | [diff] [blame] | 11 | #include "base/test/metrics/histogram_tester.h" |
Ari Chivukula | a29eb3a | 2021-07-21 02:57:48 | [diff] [blame] | 12 | #include "base/unguessable_token.h" |
Trent Apted | c998321 | 2021-06-29 02:47:58 | [diff] [blame] | 13 | #include "build/build_config.h" |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 14 | #include "chrome/browser/extensions/extension_browsertest.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 15 | #include "chrome/browser/ui/browser.h" |
| 16 | #include "chrome/browser/ui/browser_commands.h" |
| 17 | #include "chrome/browser/ui/singleton_tabs.h" |
| 18 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 19 | #include "chrome/test/base/ui_test_utils.h" |
Marijn Kruisselbrink | 604fd7e7 | 2017-10-26 16:31:05 | [diff] [blame] | 20 | #include "content/public/browser/blob_handle.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 21 | #include "content/public/browser/notification_observer.h" |
| 22 | #include "content/public/browser/notification_service.h" |
| 23 | #include "content/public/browser/notification_types.h" |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 24 | #include "content/public/browser/render_frame_host.h" |
| 25 | #include "content/public/browser/render_process_host.h" |
Nasko Oskov | d515cab | 2018-05-09 15:34:20 | [diff] [blame] | 26 | #include "content/public/browser/site_isolation_policy.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 27 | #include "content/public/browser/web_contents_observer.h" |
| 28 | #include "content/public/common/content_switches.h" |
Nasko Oskov | d83b571 | 2018-05-04 04:50:57 | [diff] [blame] | 29 | #include "content/public/common/url_constants.h" |
Peter Kasting | 919ce65 | 2020-05-07 10:22:36 | [diff] [blame] | 30 | #include "content/public/test/browser_test.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 31 | #include "content/public/test/browser_test_utils.h" |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 32 | #include "extensions/common/extension_urls.h" |
Julie Jeongeun Kim | 061709e8 | 2019-09-04 02:33:43 | [diff] [blame] | 33 | #include "mojo/public/cpp/bindings/pending_remote.h" |
Julie Jeongeun Kim | b8c0b1c | 2019-10-30 01:13:06 | [diff] [blame] | 34 | #include "mojo/public/cpp/bindings/self_owned_associated_receiver.h" |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 35 | #include "net/dns/mock_host_resolver.h" |
svaldez | a01f7d9 | 2015-11-18 17:47:56 | [diff] [blame] | 36 | #include "net/test/embedded_test_server/embedded_test_server.h" |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 37 | #include "storage/browser/blob/blob_registry_impl.h" |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 38 | #include "third_party/blink/public/common/blob/blob_utils.h" |
Marijn Kruisselbrink | cde6463 | 2018-06-22 22:45:16 | [diff] [blame] | 39 | #include "third_party/blink/public/common/features.h" |
Takuto Ikuta | 8cfb489 | 2019-01-24 01:04:05 | [diff] [blame] | 40 | #include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h" |
Blink Reformat | a30d423 | 2018-04-07 15:31:06 | [diff] [blame] | 41 | #include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h" |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 42 | |
| 43 | // The goal of these tests is to "simulate" exploited renderer processes, which |
| 44 | // can send arbitrary IPC messages and confuse browser process internal state, |
| 45 | // leading to security bugs. We are trying to verify that the browser doesn't |
| 46 | // perform any dangerous operations in such cases. |
| 47 | // This is similar to the security_exploit_browsertest.cc tests, but also |
| 48 | // includes chrome/ layer concepts such as extensions. |
Devlin Cronin | 836f545d | 2018-05-09 00:25:05 | [diff] [blame] | 49 | class ChromeSecurityExploitBrowserTest |
| 50 | : public extensions::ExtensionBrowserTest { |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 51 | public: |
| 52 | ChromeSecurityExploitBrowserTest() {} |
Peter Boström | 53c6c595 | 2021-09-17 09:41:26 | [diff] [blame^] | 53 | |
| 54 | ChromeSecurityExploitBrowserTest(const ChromeSecurityExploitBrowserTest&) = |
| 55 | delete; |
| 56 | ChromeSecurityExploitBrowserTest& operator=( |
| 57 | const ChromeSecurityExploitBrowserTest&) = delete; |
| 58 | |
dcheng | e1bc798 | 2014-10-30 00:32:40 | [diff] [blame] | 59 | ~ChromeSecurityExploitBrowserTest() override {} |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 60 | |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 61 | void SetUpOnMainThread() override { |
Devlin Cronin | 836f545d | 2018-05-09 00:25:05 | [diff] [blame] | 62 | extensions::ExtensionBrowserTest::SetUpOnMainThread(); |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 63 | |
tsergeant | bd3b7a4c | 2016-09-30 00:42:19 | [diff] [blame] | 64 | ASSERT_TRUE(embedded_test_server()->Start()); |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 65 | host_resolver()->AddRule("*", "127.0.0.1"); |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 66 | |
| 67 | extension_ = LoadExtension(test_data_dir_.AppendASCII("simple_with_icon")); |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 68 | } |
tsergeant | bd3b7a4c | 2016-09-30 00:42:19 | [diff] [blame] | 69 | |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 70 | const extensions::Extension* extension() { return extension_; } |
| 71 | |
Marijn Kruisselbrink | 604fd7e7 | 2017-10-26 16:31:05 | [diff] [blame] | 72 | std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob( |
| 73 | const std::string& contents, |
| 74 | const std::string& content_type) { |
| 75 | std::unique_ptr<content::BlobHandle> result; |
| 76 | base::RunLoop loop; |
Lukasz Anforowicz | 7ef1cfd | 2021-05-04 02:18:37 | [diff] [blame] | 77 | profile()->CreateMemoryBackedBlob( |
| 78 | base::as_bytes(base::make_span(contents)), content_type, |
Marijn Kruisselbrink | 604fd7e7 | 2017-10-26 16:31:05 | [diff] [blame] | 79 | base::BindOnce( |
| 80 | [](std::unique_ptr<content::BlobHandle>* out_blob, |
| 81 | base::OnceClosure done, |
| 82 | std::unique_ptr<content::BlobHandle> blob) { |
| 83 | *out_blob = std::move(blob); |
| 84 | std::move(done).Run(); |
| 85 | }, |
| 86 | &result, loop.QuitClosure())); |
| 87 | loop.Run(); |
| 88 | EXPECT_TRUE(result); |
| 89 | return result; |
| 90 | } |
| 91 | |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 92 | private: |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 93 | const extensions::Extension* extension_; |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 94 | }; |
| 95 | |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 96 | // Subclass of ChromeSecurityExploitBrowserTest that uses --disable-web-security |
| 97 | // to simulate an exploited renderer. Note that this also disables some browser |
| 98 | // process checks, so it's not ideal for all exploit tests. |
| 99 | class ChromeWebSecurityDisabledBrowserTest |
| 100 | : public ChromeSecurityExploitBrowserTest { |
| 101 | public: |
| 102 | ChromeWebSecurityDisabledBrowserTest() {} |
Peter Boström | 53c6c595 | 2021-09-17 09:41:26 | [diff] [blame^] | 103 | |
| 104 | ChromeWebSecurityDisabledBrowserTest( |
| 105 | const ChromeWebSecurityDisabledBrowserTest&) = delete; |
| 106 | ChromeWebSecurityDisabledBrowserTest& operator=( |
| 107 | const ChromeWebSecurityDisabledBrowserTest&) = delete; |
| 108 | |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 109 | ~ChromeWebSecurityDisabledBrowserTest() override {} |
| 110 | |
| 111 | void SetUpCommandLine(base::CommandLine* command_line) override { |
| 112 | ChromeSecurityExploitBrowserTest::SetUpCommandLine(command_line); |
| 113 | command_line->AppendSwitch(switches::kDisableWebSecurity); |
| 114 | } |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 115 | }; |
| 116 | |
Nasko Oskov | fd52b6f | 2019-02-06 19:21:15 | [diff] [blame] | 117 | // TODO(nasko): This test as written is incompatible with Site Isolation |
| 118 | // restrictions, which disallow the cross-origin pushState call. |
| 119 | // Find a different way to implement issuing the illegal request or just |
| 120 | // delete the test if we have coverage elsewhere. See https://crbug.com/929161. |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 121 | IN_PROC_BROWSER_TEST_F(ChromeWebSecurityDisabledBrowserTest, |
Nasko Oskov | fd52b6f | 2019-02-06 19:21:15 | [diff] [blame] | 122 | DISABLED_ChromeExtensionResources) { |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 123 | // Load a page that requests a chrome-extension:// image through XHR. We |
| 124 | // expect this load to fail, as it is an illegal request. |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 125 | GURL foo = embedded_test_server()->GetURL("foo.com", |
| 126 | "/chrome_extension_resource.html"); |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 127 | |
| 128 | content::DOMMessageQueue msg_queue; |
| 129 | |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 130 | ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), foo)); |
[email protected] | 5cd5634 | 2013-04-03 19:50:47 | [diff] [blame] | 131 | |
| 132 | std::string status; |
| 133 | std::string expected_status("0"); |
| 134 | EXPECT_TRUE(msg_queue.WaitForMessage(&status)); |
| 135 | EXPECT_STREQ(status.c_str(), expected_status.c_str()); |
| 136 | } |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 137 | |
Charlie Reis | e2c2c49 | 2018-06-15 21:34:04 | [diff] [blame] | 138 | // Tests that a normal web process cannot send a commit for a Chrome Web Store |
| 139 | // URL. See https://crbug.com/172119. |
| 140 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 141 | CommitWebStoreURLInWebProcess) { |
| 142 | GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| 143 | |
| 144 | content::WebContents* web_contents = |
| 145 | browser()->tab_strip_model()->GetActiveWebContents(); |
| 146 | content::RenderFrameHost* rfh = web_contents->GetMainFrame(); |
| 147 | |
| 148 | // This IPC should result in a kill because the Chrome Web Store is not |
| 149 | // allowed to commit in |rfh->GetProcess()|. |
| 150 | base::HistogramTester histograms; |
| 151 | content::RenderProcessHostWatcher crash_observer( |
| 152 | rfh->GetProcess(), |
| 153 | content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| 154 | |
| 155 | // Modify an IPC for a commit of a blank URL, which would otherwise be allowed |
| 156 | // to commit in any process. |
| 157 | GURL blank_url = GURL(url::kAboutBlankURL); |
| 158 | GURL webstore_url = extension_urls::GetWebstoreLaunchURL(); |
| 159 | content::PwnCommitIPC(web_contents, blank_url, webstore_url, |
| 160 | url::Origin::Create(GURL(webstore_url))); |
| 161 | web_contents->GetController().LoadURL( |
| 162 | blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); |
| 163 | |
| 164 | // If the process is killed in CanCommitURL, this test passes. |
| 165 | crash_observer.Wait(); |
| 166 | histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1); |
| 167 | } |
| 168 | |
| 169 | // Tests that a non-extension process cannot send a commit of a blank URL with |
| 170 | // an extension origin. |
| 171 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 172 | CommitExtensionOriginInWebProcess) { |
| 173 | GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| 174 | |
| 175 | content::WebContents* web_contents = |
| 176 | browser()->tab_strip_model()->GetActiveWebContents(); |
| 177 | content::RenderFrameHost* rfh = web_contents->GetMainFrame(); |
| 178 | |
| 179 | // This IPC should result in a kill because |ext_origin| is not allowed to |
| 180 | // commit in |rfh->GetProcess()|. |
| 181 | base::HistogramTester histograms; |
| 182 | content::RenderProcessHostWatcher crash_observer( |
| 183 | rfh->GetProcess(), |
| 184 | content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| 185 | |
| 186 | // Modify an IPC for a commit of a blank URL, which would otherwise be allowed |
| 187 | // to commit in any process. |
| 188 | GURL blank_url = GURL(url::kAboutBlankURL); |
| 189 | std::string ext_origin = "chrome-extension://" + extension()->id(); |
| 190 | content::PwnCommitIPC(web_contents, blank_url, blank_url, |
| 191 | url::Origin::Create(GURL(ext_origin))); |
| 192 | web_contents->GetController().LoadURL( |
| 193 | blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); |
| 194 | |
| 195 | // If the process is killed in CanCommitOrigin, this test passes. |
| 196 | crash_observer.Wait(); |
| 197 | histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 114, |
| 198 | 1); |
| 199 | } |
| 200 | |
| 201 | // Tests that a non-extension process cannot send a commit of an extension URL. |
| 202 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 203 | CommitExtensionURLInWebProcess) { |
| 204 | GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| 205 | |
| 206 | content::WebContents* web_contents = |
| 207 | browser()->tab_strip_model()->GetActiveWebContents(); |
| 208 | content::RenderFrameHost* rfh = web_contents->GetMainFrame(); |
| 209 | |
| 210 | // This IPC should result in a kill because extension URLs are not allowed to |
| 211 | // commit in |rfh->GetProcess()|. |
| 212 | base::HistogramTester histograms; |
| 213 | content::RenderProcessHostWatcher crash_observer( |
| 214 | rfh->GetProcess(), |
| 215 | content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| 216 | |
| 217 | // Modify an IPC for a commit of a blank URL, which would otherwise be allowed |
| 218 | // to commit in any process. |
| 219 | GURL blank_url = GURL(url::kAboutBlankURL); |
| 220 | std::string ext_origin = "chrome-extension://" + extension()->id(); |
| 221 | content::PwnCommitIPC(web_contents, blank_url, GURL(ext_origin), |
| 222 | url::Origin::Create(GURL(ext_origin))); |
| 223 | web_contents->GetController().LoadURL( |
| 224 | blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); |
| 225 | |
| 226 | // If the process is killed in CanCommitURL, this test passes. |
| 227 | crash_observer.Wait(); |
| 228 | histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1); |
| 229 | } |
| 230 | |
| 231 | // Tests that a non-extension process cannot send a commit of an extension |
| 232 | // filesystem URL. |
| 233 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 234 | CommitExtensionFilesystemURLInWebProcess) { |
| 235 | GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| 236 | |
| 237 | content::WebContents* web_contents = |
| 238 | browser()->tab_strip_model()->GetActiveWebContents(); |
| 239 | content::RenderFrameHost* rfh = web_contents->GetMainFrame(); |
| 240 | |
| 241 | // This IPC should result in a kill because extension filesystem URLs are not |
| 242 | // allowed to commit in |rfh->GetProcess()|. |
| 243 | base::HistogramTester histograms; |
| 244 | content::RenderProcessHostWatcher crash_observer( |
| 245 | rfh->GetProcess(), |
| 246 | content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| 247 | |
| 248 | // Modify an IPC for a commit of a blank URL, which would otherwise be allowed |
| 249 | // to commit in any process. |
| 250 | GURL blank_url = GURL(url::kAboutBlankURL); |
| 251 | std::string ext_origin = "chrome-extension://" + extension()->id(); |
| 252 | content::PwnCommitIPC(web_contents, blank_url, |
| 253 | GURL("filesystem:" + ext_origin + "/foo"), |
| 254 | url::Origin::Create(GURL(ext_origin))); |
| 255 | web_contents->GetController().LoadURL( |
| 256 | blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); |
| 257 | |
| 258 | // If the process is killed in CanCommitURL, this test passes. |
| 259 | crash_observer.Wait(); |
| 260 | histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1); |
| 261 | } |
| 262 | |
Daniel Cheng | 4ebba55 | 2018-07-06 21:43:16 | [diff] [blame] | 263 | // chrome://xyz should not be able to create a "filesystem:chrome://abc" |
| 264 | // resource. |
| 265 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 266 | CreateFilesystemURLInOtherChromeUIOrigin) { |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 267 | ASSERT_TRUE( |
| 268 | ui_test_utils::NavigateToURL(browser(), GURL("chrome://version"))); |
Daniel Cheng | 4ebba55 | 2018-07-06 21:43:16 | [diff] [blame] | 269 | |
| 270 | content::RenderFrameHost* rfh = |
| 271 | browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
| 272 | |
| 273 | // Block the renderer on operation that never completes, to shield it from |
| 274 | // receiving unexpected browser->renderer IPCs that might CHECK. |
| 275 | rfh->ExecuteJavaScriptWithUserGestureForTests( |
Peter Kasting | aae6db93 | 2021-05-04 12:02:11 | [diff] [blame] | 276 | u"var r = new XMLHttpRequest();" |
| 277 | u"r.open('GET', '/slow?99999', false);" |
| 278 | u"r.send(null);" |
| 279 | u"while (1);"); |
Daniel Cheng | 4ebba55 | 2018-07-06 21:43:16 | [diff] [blame] | 280 | |
| 281 | std::string payload = "<p>Hello world!</p>"; |
| 282 | std::string payload_type = "text/html"; |
| 283 | |
| 284 | // Target an extension. |
| 285 | std::string target_origin = "chrome://downloads"; |
| 286 | |
| 287 | // Set up a blob ID and populate it with the attacker-controlled payload. This |
| 288 | // is just using the blob APIs directly since creating arbitrary blobs is not |
| 289 | // what is prohibited; this data is not in any origin. |
| 290 | std::unique_ptr<content::BlobHandle> blob = |
| 291 | CreateMemoryBackedBlob(payload, payload_type); |
| 292 | std::string blob_id = blob->GetUUID(); |
| 293 | |
| 294 | // Note: a well-behaved renderer would always send the following message here, |
| 295 | // but it's actually not necessary for the original attack to succeed, so we |
| 296 | // omit it. As a result there are some log warnings from the quota observer. |
| 297 | // |
| 298 | // IPC::IpcSecurityTestUtil::PwnMessageReceived( |
| 299 | // rfh->GetProcess()->GetChannel(), |
| 300 | // FileSystemHostMsg_OpenFileSystem(22, GURL(target_origin), |
| 301 | // storage::kFileSystemTypeTemporary)); |
| 302 | |
| 303 | GURL target_url = |
| 304 | GURL("filesystem:" + target_origin + "/temporary/exploit.html"); |
| 305 | |
| 306 | content::PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, |
| 307 | false, false, false); |
| 308 | |
| 309 | // Write the blob into the file. If successful, this places an |
| 310 | // attacker-controlled value in a resource on the extension origin. |
| 311 | content::PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, |
| 312 | blob_id, 0); |
| 313 | |
| 314 | // Now navigate to |target_url| in a new tab. It should not contain |payload|. |
| 315 | AddTabAtIndex(0, target_url, ui::PAGE_TRANSITION_TYPED); |
Synthia Islam | 2761aac | 2020-02-25 18:48:32 | [diff] [blame] | 316 | EXPECT_FALSE(content::WaitForLoadStop( |
| 317 | browser()->tab_strip_model()->GetWebContentsAt(0))); |
Daniel Cheng | 4ebba55 | 2018-07-06 21:43:16 | [diff] [blame] | 318 | rfh = browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
| 319 | |
| 320 | // If the attack is unsuccessful, the navigation ends up in an error |
| 321 | // page. |
| 322 | if (content::SiteIsolationPolicy::IsErrorPageIsolationEnabled( |
| 323 | !rfh->GetParent())) { |
| 324 | EXPECT_EQ(GURL(content::kUnreachableWebDataURL), |
| 325 | rfh->GetSiteInstance()->GetSiteURL()); |
| 326 | } else { |
| 327 | EXPECT_EQ(GURL(target_origin), rfh->GetSiteInstance()->GetSiteURL()); |
| 328 | } |
| 329 | std::string body; |
| 330 | std::string script = R"( |
| 331 | var textContent = document.body.innerText.replace(/\n+/g, '\n'); |
| 332 | window.domAutomationController.send(textContent); |
| 333 | )"; |
| 334 | |
| 335 | EXPECT_TRUE(content::ExecuteScriptAndExtractString(rfh, script, &body)); |
| 336 | EXPECT_EQ( |
Lily Chen | 09b5ce4 | 2020-07-16 06:28:41 | [diff] [blame] | 337 | "Your file couldn’t be accessed\n" |
| 338 | "It may have been moved, edited, or deleted.\n" |
Yoshifumi Inoue | 72d438e | 2018-08-24 08:17:29 | [diff] [blame] | 339 | "ERR_FILE_NOT_FOUND", |
Daniel Cheng | 4ebba55 | 2018-07-06 21:43:16 | [diff] [blame] | 340 | body); |
| 341 | } |
| 342 | |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 343 | // Extension isolation prevents a normal renderer process from being able to |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 344 | // create a "filesystem:chrome-extension://sdgkjaghsdg/temporary/" resource. |
| 345 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest, |
| 346 | CreateFilesystemURLInExtensionOrigin) { |
| 347 | GURL page_url = |
| 348 | embedded_test_server()->GetURL("a.root-servers.net", "/title1.html"); |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 349 | ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url)); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 350 | |
| 351 | content::RenderFrameHost* rfh = |
| 352 | browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
| 353 | |
| 354 | // Block the renderer on operation that never completes, to shield it from |
| 355 | // receiving unexpected browser->renderer IPCs that might CHECK. |
| 356 | rfh->ExecuteJavaScriptWithUserGestureForTests( |
Peter Kasting | aae6db93 | 2021-05-04 12:02:11 | [diff] [blame] | 357 | u"var r = new XMLHttpRequest();" |
| 358 | u"r.open('GET', '/slow?99999', false);" |
| 359 | u"r.send(null);" |
| 360 | u"while (1);"); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 361 | |
| 362 | // JS code that the attacker would like to run in an extension process. |
| 363 | std::string payload = "<html><body>pwned.</body></html>"; |
| 364 | std::string payload_type = "text/html"; |
| 365 | |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 366 | // Target an extension. |
| 367 | std::string target_origin = "chrome-extension://" + extension()->id(); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 368 | |
Marijn Kruisselbrink | 604fd7e7 | 2017-10-26 16:31:05 | [diff] [blame] | 369 | // Set up a blob ID and populate it with the attacker-controlled payload. This |
| 370 | // is just using the blob APIs directly since creating arbitrary blobs is not |
| 371 | // what is prohibited; this data is not in any origin. |
| 372 | std::unique_ptr<content::BlobHandle> blob = |
| 373 | CreateMemoryBackedBlob(payload, payload_type); |
| 374 | std::string blob_id = blob->GetUUID(); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 375 | |
Adithya Srinivasan | 0c72ff0 | 2018-08-13 19:47:29 | [diff] [blame] | 376 | // Note: a well-behaved renderer would always call Open first before calling |
| 377 | // Create and Write, but it's actually not necessary for the original attack |
| 378 | // to succeed, so we omit it. As a result there are some log warnings from the |
| 379 | // quota observer. |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 380 | |
| 381 | GURL target_url = |
calamity | ae7fed4 | 2017-06-22 04:58:22 | [diff] [blame] | 382 | GURL("filesystem:" + target_origin + "/temporary/exploit.html"); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 383 | |
| 384 | content::PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, |
| 385 | false, false, false); |
| 386 | |
| 387 | // Write the blob into the file. If successful, this places an |
| 388 | // attacker-controlled value in a resource on the extension origin. |
| 389 | content::PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, |
| 390 | blob_id, 0); |
| 391 | |
| 392 | // Now navigate to |target_url| in a new tab. It should not contain |payload|. |
| 393 | AddTabAtIndex(0, target_url, ui::PAGE_TRANSITION_TYPED); |
Synthia Islam | 2761aac | 2020-02-25 18:48:32 | [diff] [blame] | 394 | EXPECT_FALSE(content::WaitForLoadStop( |
| 395 | browser()->tab_strip_model()->GetWebContentsAt(0))); |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 396 | rfh = browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
Nasko Oskov | d83b571 | 2018-05-04 04:50:57 | [diff] [blame] | 397 | |
| 398 | // If the attack is unsuccessful, the navigation ends up in an error |
| 399 | // page. |
Nasko Oskov | d515cab | 2018-05-09 15:34:20 | [diff] [blame] | 400 | if (content::SiteIsolationPolicy::IsErrorPageIsolationEnabled( |
| 401 | !rfh->GetParent())) { |
| 402 | EXPECT_EQ(GURL(content::kUnreachableWebDataURL), |
| 403 | rfh->GetSiteInstance()->GetSiteURL()); |
| 404 | } else { |
| 405 | EXPECT_EQ(GURL(target_origin), rfh->GetSiteInstance()->GetSiteURL()); |
| 406 | } |
nick | bfaea4ee | 2016-12-02 20:59:31 | [diff] [blame] | 407 | std::string body; |
ntfschr | d9f4dd5 | 2017-05-02 16:21:27 | [diff] [blame] | 408 | std::string script = R"( |
| 409 | var textContent = document.body.innerText.replace(/\n+/g, '\n'); |
| 410 | window.domAutomationController.send(textContent); |
| 411 | )"; |
| 412 | |
| 413 | EXPECT_TRUE(content::ExecuteScriptAndExtractString(rfh, script, &body)); |
nasko | abed2a5 | 2017-05-03 05:10:17 | [diff] [blame] | 414 | EXPECT_EQ( |
Lily Chen | 09b5ce4 | 2020-07-16 06:28:41 | [diff] [blame] | 415 | "Your file couldn’t be accessed\n" |
| 416 | "It may have been moved, edited, or deleted.\n" |
Yoshifumi Inoue | 72d438e | 2018-08-24 08:17:29 | [diff] [blame] | 417 | "ERR_FILE_NOT_FOUND", |
nasko | abed2a5 | 2017-05-03 05:10:17 | [diff] [blame] | 418 | body); |
nick | 2a8ba8c | 2016-10-03 18:51:39 | [diff] [blame] | 419 | } |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 420 | |
| 421 | namespace { |
| 422 | |
| 423 | class BlobURLStoreInterceptor |
| 424 | : public blink::mojom::BlobURLStoreInterceptorForTesting { |
| 425 | public: |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 426 | static void Intercept( |
| 427 | GURL target_url, |
Julie Jeongeun Kim | b8c0b1c | 2019-10-30 01:13:06 | [diff] [blame] | 428 | mojo::SelfOwnedAssociatedReceiverRef<blink::mojom::BlobURLStore> |
| 429 | receiver) { |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 430 | auto interceptor = |
| 431 | base::WrapUnique(new BlobURLStoreInterceptor(target_url)); |
| 432 | auto* raw_interceptor = interceptor.get(); |
Julie Jeongeun Kim | b8c0b1c | 2019-10-30 01:13:06 | [diff] [blame] | 433 | auto impl = receiver->SwapImplForTesting(std::move(interceptor)); |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 434 | raw_interceptor->url_store_ = std::move(impl); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 435 | } |
| 436 | |
| 437 | blink::mojom::BlobURLStore* GetForwardingInterface() override { |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 438 | return url_store_.get(); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 439 | } |
| 440 | |
Ari Chivukula | a29eb3a | 2021-07-21 02:57:48 | [diff] [blame] | 441 | void Register( |
| 442 | mojo::PendingRemote<blink::mojom::Blob> blob, |
| 443 | const GURL& url, |
| 444 | // TODO(https://crbug.com/1224926): Remove this once experiment is over. |
| 445 | const base::UnguessableToken& unsafe_agent_cluster_id, |
| 446 | RegisterCallback callback) override { |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 447 | GetForwardingInterface()->Register(std::move(blob), target_url_, |
Ari Chivukula | a29eb3a | 2021-07-21 02:57:48 | [diff] [blame] | 448 | unsafe_agent_cluster_id, |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 449 | std::move(callback)); |
| 450 | } |
| 451 | |
| 452 | private: |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 453 | explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {} |
| 454 | |
| 455 | std::unique_ptr<blink::mojom::BlobURLStore> url_store_; |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 456 | GURL target_url_; |
| 457 | }; |
| 458 | |
| 459 | } // namespace |
| 460 | |
| 461 | class ChromeSecurityExploitBrowserTestMojoBlobURLs |
| 462 | : public ChromeSecurityExploitBrowserTest { |
| 463 | public: |
Kinuko Yasuda | 7d925ea2 | 2019-08-01 10:08:48 | [diff] [blame] | 464 | ChromeSecurityExploitBrowserTestMojoBlobURLs() = default; |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 465 | |
| 466 | void TearDown() override { |
| 467 | storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(nullptr); |
| 468 | } |
| 469 | }; |
| 470 | |
| 471 | // Extension isolation prevents a normal renderer process from being able to |
| 472 | // create a "blob:chrome-extension://" resource. |
| 473 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTestMojoBlobURLs, |
| 474 | CreateBlobInExtensionOrigin) { |
| 475 | // Target an extension. |
| 476 | std::string target_origin = "chrome-extension://" + extension()->id(); |
| 477 | std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd"; |
Lucas Furukawa Gadani | ebc8f7fc3 | 2019-06-18 18:06:48 | [diff] [blame] | 478 | auto intercept_hook = |
| 479 | base::BindRepeating(&BlobURLStoreInterceptor::Intercept, |
| 480 | GURL("blob:" + target_origin + "/" + blob_path)); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 481 | storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook); |
| 482 | |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 483 | ASSERT_TRUE(ui_test_utils::NavigateToURL( |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 484 | browser(), |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 485 | embedded_test_server()->GetURL("a.root-servers.net", "/title1.html"))); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 486 | |
| 487 | content::RenderFrameHost* rfh = |
| 488 | browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
| 489 | |
Lukasz Anforowicz | 110cda3 | 2020-03-23 22:32:03 | [diff] [blame] | 490 | content::RenderProcessHostBadMojoMessageWaiter crash_observer( |
| 491 | rfh->GetProcess()); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 492 | |
| 493 | // The renderer should always get killed, but sometimes ExecuteScript returns |
| 494 | // true anyway, so just ignore the result. |
| 495 | ignore_result( |
| 496 | content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))")); |
| 497 | |
| 498 | // If the process is killed, this test passes. |
Lukasz Anforowicz | 110cda3 | 2020-03-23 22:32:03 | [diff] [blame] | 499 | EXPECT_EQ( |
| 500 | "Received bad user message: Non committable URL passed to " |
| 501 | "BlobURLStore::Register", |
| 502 | crash_observer.Wait()); |
Marijn Kruisselbrink | 8f1b1a7 | 2018-01-26 18:09:56 | [diff] [blame] | 503 | } |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 504 | |
Trent Apted | c998321 | 2021-06-29 02:47:58 | [diff] [blame] | 505 | // Flaky. See https://crbug.com/1224293. |
| 506 | #if defined(OS_CHROMEOS) |
| 507 | #define MAYBE_CreateBlobInOtherChromeUIOrigin \ |
| 508 | DISABLED_CreateBlobInOtherChromeUIOrigin |
| 509 | #else |
| 510 | #define MAYBE_CreateBlobInOtherChromeUIOrigin CreateBlobInOtherChromeUIOrigin |
| 511 | #endif // OS_CHROMEOS |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 512 | // chrome://xyz should not be able to create a "blob:chrome://abc" resource. |
| 513 | IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTestMojoBlobURLs, |
Trent Apted | c998321 | 2021-06-29 02:47:58 | [diff] [blame] | 514 | MAYBE_CreateBlobInOtherChromeUIOrigin) { |
Lukasz Anforowicz | b78290c | 2021-09-08 04:31:38 | [diff] [blame] | 515 | ASSERT_TRUE( |
| 516 | ui_test_utils::NavigateToURL(browser(), GURL("chrome://version"))); |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 517 | |
| 518 | // All these are attacker controlled values. |
| 519 | std::string blob_type = "text/html"; |
| 520 | std::string blob_contents = "<p>Hello world!</p>"; |
| 521 | std::string blob_path = "f7dfbeb5-8e41-4c4a-8486-a52fed33c4c0"; |
| 522 | |
| 523 | // Target an extension. |
| 524 | std::string target_origin = "chrome://downloads"; |
| 525 | |
| 526 | auto intercept_hook = |
| 527 | base::BindRepeating(&BlobURLStoreInterceptor::Intercept, |
| 528 | GURL("blob:" + target_origin + "/" + blob_path)); |
| 529 | storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook); |
| 530 | |
| 531 | content::RenderFrameHost* rfh = |
| 532 | browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(); |
| 533 | |
Lukasz Anforowicz | 110cda3 | 2020-03-23 22:32:03 | [diff] [blame] | 534 | content::RenderProcessHostBadMojoMessageWaiter crash_observer( |
| 535 | rfh->GetProcess()); |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 536 | |
| 537 | // The renderer should always get killed, but sometimes ExecuteScript returns |
| 538 | // true anyway, so just ignore the result. |
| 539 | ignore_result( |
| 540 | content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))")); |
| 541 | |
| 542 | // If the process is killed, this test passes. |
Lukasz Anforowicz | 110cda3 | 2020-03-23 22:32:03 | [diff] [blame] | 543 | EXPECT_EQ( |
| 544 | "Received bad user message: Non committable URL passed to " |
| 545 | "BlobURLStore::Register", |
| 546 | crash_observer.Wait()); |
Kinuko Yasuda | 04a82ab | 2019-07-26 06:13:39 | [diff] [blame] | 547 | } |