| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/command_line.h" |
| #include "base/macros.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/resource_type.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| namespace content { |
| |
| // These tests verify that the browser process blocks cross-site HTML, XML, |
| // JSON, and some plain text responses when they are not otherwise permitted |
| // (e.g., by CORS). This ensures that such responses never end up in the |
| // renderer process where they might be accessible via a bug. Careful attention |
| // is paid to allow other cross-site resources necessary for rendering, |
| // including cases that may be mislabeled as blocked MIME type. |
| // |
| // Many of these tests work by turning off the Same Origin Policy in the |
| // renderer process via --disable-web-security, and then trying to access the |
| // resource via a cross-origin XHR. If the response is blocked, the XHR should |
| // see an empty response body. |
| // |
| // Note that this BaseTest class does not specify an isolation mode via |
| // command-line flags. Most of the tests are in the --site-per-process subclass |
| // below. |
| class CrossSiteDocumentBlockingBaseTest : public ContentBrowserTest { |
| public: |
| CrossSiteDocumentBlockingBaseTest() {} |
| ~CrossSiteDocumentBlockingBaseTest() override {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_| |
| // which is required below. This cannot invoke Start() however as that kicks |
| // off the "EmbeddedTestServer IO Thread" which then races with |
| // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545. |
| ASSERT_TRUE(embedded_test_server()->InitializeAndListen()); |
| |
| // Add a host resolver rule to map all outgoing requests to the test server. |
| // This allows us to use "real" hostnames and standard ports in URLs (i.e., |
| // without having to inject the port number into all URLs), which we can use |
| // to create arbitrary SiteInstances. |
| command_line->AppendSwitchASCII( |
| network::switches::kHostResolverRules, |
| "MAP * " + embedded_test_server()->host_port_pair().ToString() + |
| ",EXCLUDE localhost"); |
| |
| // To test that the renderer process does not receive blocked documents, we |
| // disable the same origin policy to let it see cross-origin fetches if they |
| // are received. |
| command_line->AppendSwitch(switches::kDisableWebSecurity); |
| } |
| |
| void SetUpOnMainThread() override { |
| // Complete the manual Start() after ContentBrowserTest's own |
| // initialization, ref. comment on InitializeAndListen() above. |
| embedded_test_server()->StartAcceptingConnections(); |
| } |
| |
| // Ensure the correct histograms are incremented for blocking events. |
| // Assumes the resource type is XHR. |
| void InspectHistograms(const base::HistogramTester& histograms, |
| bool should_be_blocked, |
| bool should_be_sniffed, |
| const std::string& resource_name, |
| ResourceType resource_type) { |
| std::string bucket; |
| if (base::MatchPattern(resource_name, "*.html")) { |
| bucket = "HTML"; |
| } else if (base::MatchPattern(resource_name, "*.xml")) { |
| bucket = "XML"; |
| } else if (base::MatchPattern(resource_name, "*.json")) { |
| bucket = "JSON"; |
| } else if (base::MatchPattern(resource_name, "*.txt")) { |
| bucket = "Plain"; |
| } else { |
| bucket = "Others"; |
| } |
| |
| // Determine the appropriate histograms, including a start and end action |
| // (which are verified in unit tests), a read size if it was sniffed, and |
| // additional blocked metrics if it was blocked. |
| base::HistogramTester::CountsMap expected_counts; |
| std::string base = "SiteIsolation.XSD.Browser"; |
| expected_counts[base + ".Action"] = 2; |
| if ((base::MatchPattern(resource_name, "*prefixed*") || |
| bucket == "Others") && |
| should_be_blocked) { |
| expected_counts[base + ".BlockedForParserBreaker"] = 1; |
| } |
| if (should_be_sniffed) |
| expected_counts[base + ".BytesReadForSniffing"] = 1; |
| if (should_be_blocked) { |
| expected_counts[base + ".Blocked"] = 1; |
| expected_counts[base + ".Blocked." + bucket] = 1; |
| } |
| |
| // Make sure that the expected metrics, and only those metrics, were |
| // incremented. |
| EXPECT_THAT(histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser"), |
| testing::ContainerEq(expected_counts)) |
| << "For resource_name=" << resource_name |
| << ", should_be_blocked=" << should_be_blocked; |
| |
| // Determine if the bucket for the resource type (XHR) was incremented. |
| if (should_be_blocked) { |
| EXPECT_THAT(histograms.GetAllSamples(base + ".Blocked"), |
| testing::ElementsAre(base::Bucket(resource_type, 1))) |
| << "The wrong Blocked bucket was incremented."; |
| EXPECT_THAT(histograms.GetAllSamples(base + ".Blocked." + bucket), |
| testing::ElementsAre(base::Bucket(resource_type, 1))) |
| << "The wrong Blocked bucket was incremented."; |
| } |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentBlockingBaseTest); |
| }; |
| |
| // Most tests here use --site-per-process, which enables document blocking |
| // everywhere. |
| class CrossSiteDocumentBlockingTest : public CrossSiteDocumentBlockingBaseTest { |
| public: |
| CrossSiteDocumentBlockingTest() {} |
| ~CrossSiteDocumentBlockingTest() override {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| IsolateAllSitesForTesting(command_line); |
| CrossSiteDocumentBlockingBaseTest::SetUpCommandLine(command_line); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentBlockingTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingTest, BlockDocuments) { |
| // Load a page that issues illegal cross-site document requests to bar.com. |
| // The page uses XHR to request HTML/XML/JSON documents from bar.com, and |
| // inspects if any of them were successfully received. This test is only |
| // possible since we run the browser without the same origin policy, allowing |
| // it to see the response body if it makes it to the renderer (even if the |
| // renderer would normally block access to it). |
| GURL foo_url("http://foo.com/cross_site_document_request.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| // The following are files under content/test/data/site_isolation. All |
| // should be disallowed for cross site XHR under the document blocking policy. |
| // valid.* - Correctly labeled HTML/XML/JSON files. |
| // *.txt - Plain text that sniffs as HTML, XML, or JSON. |
| // htmlN_dtd.* - Various HTML templates to test. |
| // json-prefixed* - parser-breaking prefixes |
| const char* blocked_resources[] = {"valid.html", |
| "valid.xml", |
| "valid.json", |
| "html.txt", |
| "xml.txt", |
| "json.txt", |
| "comment_valid.html", |
| "html4_dtd.html", |
| "html4_dtd.txt", |
| "html5_dtd.html", |
| "html5_dtd.txt", |
| "json.js", |
| "json-prefixed-1.js", |
| "json-prefixed-2.js", |
| "json-prefixed-3.js", |
| "json-prefixed-4.js", |
| "nosniff.json.js", |
| "nosniff.json-prefixed.js"}; |
| for (const char* resource : blocked_resources) { |
| SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource)); |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), base::StringPrintf("sendRequest('%s');", resource), |
| &was_blocked)); |
| EXPECT_TRUE(was_blocked); |
| InspectHistograms(histograms, true /* should_be_blocked */, |
| true /* should_be_sniffed */, resource, |
| RESOURCE_TYPE_XHR); |
| } |
| |
| // These files should be disallowed without sniffing. |
| // nosniff.* - Won't sniff correctly, but blocked because of nosniff. |
| const char* nosniff_blocked_resources[] = {"nosniff.html", "nosniff.xml", |
| "nosniff.json", "nosniff.txt"}; |
| for (const char* resource : nosniff_blocked_resources) { |
| SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource)); |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), base::StringPrintf("sendRequest('%s');", resource), |
| &was_blocked)); |
| EXPECT_TRUE(was_blocked); |
| InspectHistograms(histograms, true /* should_be_blocked */, |
| false /* should_be_sniffed */, resource, |
| RESOURCE_TYPE_XHR); |
| } |
| |
| // These files are allowed for XHR under the document blocking policy because |
| // the sniffing logic determines they are not actually documents. |
| // *js.* - JavaScript mislabeled as a document. |
| // jsonp.* - JSONP (i.e., script) mislabeled as a document. |
| // img.* - Contents that won't match the document label. |
| // valid.* - Correctly labeled responses of non-document types. |
| const char* sniff_allowed_resources[] = { |
| "js.html", "comment_js.html", "js.xml", "js.json", |
| "js.txt", "jsonp.html", "jsonp.xml", "jsonp.json", |
| "jsonp.txt", "img.html", "img.xml", "img.json", |
| "img.txt", "valid.js", "json-list.js", "nosniff.json-list.js"}; |
| for (const char* resource : sniff_allowed_resources) { |
| SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource)); |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), base::StringPrintf("sendRequest('%s');", resource), |
| &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| InspectHistograms(histograms, false /* should_be_blocked */, |
| true /* should_be_sniffed */, resource, |
| RESOURCE_TYPE_XHR); |
| } |
| |
| // These files should be allowed for XHR under the document blocking policy. |
| // cors.* - Correctly labeled documents with valid CORS headers. |
| const char* allowed_resources[] = {"cors.html", "cors.xml", "cors.json", |
| "cors.txt"}; |
| for (const char* resource : allowed_resources) { |
| SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource)); |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), base::StringPrintf("sendRequest('%s');", resource), |
| &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| InspectHistograms(histograms, false /* should_be_blocked */, |
| false /* should_be_sniffed */, resource, |
| RESOURCE_TYPE_XHR); |
| } |
| } |
| |
| // Verify that range requests disable the sniffing logic, so that attackers |
| // can't cause sniffing to fail to force a response to be allowed. This won't |
| // be a problem for script files mislabeled as HTML/XML/JSON/text (i.e., the |
| // reason for sniffing), since script tags won't send Range headers. |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingTest, RangeRequest) { |
| GURL foo_url("http://foo.com/cross_site_document_request.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| { |
| // Try to skip the first byte using a range request in an attempt to get the |
| // response to fail sniffing and be allowed through. It should still be |
| // blocked because sniffing is disabled. |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest('valid.html', 'bytes=1-24');", &was_blocked)); |
| EXPECT_TRUE(was_blocked); |
| InspectHistograms(histograms, true /* should_be_blocked */, |
| false /* should_be_sniffed */, "valid.html", |
| RESOURCE_TYPE_XHR); |
| } |
| { |
| // Verify that a response which would have been allowed by MIME type anyway |
| // is still allowed for range requests. |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest('valid.js', 'bytes=1-5');", &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| InspectHistograms(histograms, false /* should_be_blocked */, |
| false /* should_be_sniffed */, "valid.js", |
| RESOURCE_TYPE_XHR); |
| } |
| { |
| // Verify that a response which would have been allowed by CORS anyway is |
| // still allowed for range requests. |
| base::HistogramTester histograms; |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest('cors.json', 'bytes=2-7');", &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| InspectHistograms(histograms, false /* should_be_blocked */, |
| false /* should_be_sniffed */, "cors.json", |
| RESOURCE_TYPE_XHR); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingTest, BlockForVariousTargets) { |
| // This webpage loads a cross-site HTML page in different targets such as |
| // <img>,<link>,<embed>, etc. Since the requested document is blocked, and one |
| // character string (' ') is returned instead, this tests that the renderer |
| // does not crash even when it receives a response body which is " ", whose |
| // length is different from what's described in "content-length" for such |
| // different targets. |
| |
| // TODO(nick): Split up these cases, and add positive assertions here about |
| // what actually happens in these various resource-block cases. |
| GURL foo("http://foo.com/cross_site_document_request_target.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo)); |
| WaitForLoadStop(shell()->web_contents()); |
| |
| // TODO(creis): Wait for all the subresources to load and ensure renderer |
| // process is still alive. |
| } |
| |
| class CrossSiteDocumentBlockingKillSwitchTest |
| : public CrossSiteDocumentBlockingTest { |
| public: |
| CrossSiteDocumentBlockingKillSwitchTest() { |
| // Simulate flipping the kill switch. |
| scoped_feature_list_.InitAndDisableFeature( |
| features::kCrossSiteDocumentBlockingIfIsolating); |
| } |
| |
| ~CrossSiteDocumentBlockingKillSwitchTest() override {} |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentBlockingKillSwitchTest); |
| }; |
| |
| // After the kill switch is flipped, there should be no document blocking. |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingKillSwitchTest, |
| NoBlockingWithKillSwitch) { |
| // Load a page that issues illegal cross-site document requests to bar.com. |
| GURL foo_url("http://foo.com/cross_site_document_request.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest(\"valid.html\");", &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| } |
| |
| // Without any Site Isolation (in the base test class), there should be no |
| // document blocking. |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingBaseTest, |
| DontBlockDocumentsByDefault) { |
| if (AreAllSitesIsolatedForTesting()) |
| return; |
| |
| // Load a page that issues illegal cross-site document requests to bar.com. |
| GURL foo_url("http://foo.com/cross_site_document_request.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest(\"valid.html\");", &was_blocked)); |
| EXPECT_FALSE(was_blocked); |
| } |
| |
| // Test class to verify that documents are blocked for isolated origins as well. |
| class CrossSiteDocumentBlockingIsolatedOriginTest |
| : public CrossSiteDocumentBlockingBaseTest { |
| public: |
| CrossSiteDocumentBlockingIsolatedOriginTest() {} |
| ~CrossSiteDocumentBlockingIsolatedOriginTest() override {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitchASCII(switches::kIsolateOrigins, |
| "http://bar.com"); |
| CrossSiteDocumentBlockingBaseTest::SetUpCommandLine(command_line); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentBlockingIsolatedOriginTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingIsolatedOriginTest, |
| BlockDocumentsFromIsolatedOrigin) { |
| if (AreAllSitesIsolatedForTesting()) |
| return; |
| |
| // Load a page that issues illegal cross-site document requests to the |
| // isolated origin. |
| GURL foo_url("http://foo.com/cross_site_document_request.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| bool was_blocked; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), "sendRequest(\"valid.html\");", &was_blocked)); |
| EXPECT_TRUE(was_blocked); |
| } |
| |
| } // namespace content |