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