| // 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 <ostream> |
| #include <string> |
| #include <utility> |
| |
| #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 "build/build_config.h" |
| #include "content/browser/loader/cross_site_document_resource_handler.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.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_navigation_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/url_loader_interceptor.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 "services/network/test/test_url_loader_client.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| namespace content { |
| |
| using testing::Not; |
| using testing::HasSubstr; |
| |
| namespace { |
| |
| enum HistogramExpectations { |
| kShouldBeBlocked = 1 << 0, |
| kShouldBeSniffed = 1 << 1, |
| kShouldHaveContentLength = 1 << 2, |
| |
| kShouldBeAllowedWithoutSniffing = 0, |
| kShouldBeBlockedWithoutSniffing = kShouldBeBlocked, |
| kShouldBeSniffedAndAllowed = kShouldBeSniffed, |
| kShouldBeSniffedAndBlocked = kShouldBeSniffed | kShouldBeBlocked, |
| }; |
| |
| HistogramExpectations operator|(HistogramExpectations a, |
| HistogramExpectations b) { |
| return static_cast<HistogramExpectations>(static_cast<int>(a) | |
| static_cast<int>(b)); |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const HistogramExpectations& value) { |
| if (value == 0) { |
| os << "(none)"; |
| return os; |
| } |
| |
| os << "( "; |
| if (0 != (value & kShouldBeBlocked)) |
| os << "kShouldBeBlocked "; |
| if (0 != (value & kShouldBeSniffed)) |
| os << "kShouldBeSniffed "; |
| if (0 != (value & kShouldHaveContentLength)) |
| os << "kShouldHaveContentLength "; |
| os << ")"; |
| return os; |
| } |
| |
| // Ensure the correct histograms are incremented for blocking events. |
| // Assumes the resource type is XHR. |
| void InspectHistograms(const base::HistogramTester& histograms, |
| const HistogramExpectations& expectations, |
| 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") && |
| (0 != (expectations & kShouldBeBlocked))) { |
| expected_counts[base + ".BlockedForParserBreaker"] = 1; |
| } |
| if (0 != (expectations & kShouldBeSniffed)) |
| expected_counts[base + ".BytesReadForSniffing"] = 1; |
| if (0 != (expectations & kShouldBeBlocked)) { |
| expected_counts[base + ".Blocked"] = 1; |
| expected_counts[base + ".Blocked." + bucket] = 1; |
| expected_counts[base + ".Blocked.ContentLength.WasAvailable"] = 1; |
| if (0 != (expectations & kShouldHaveContentLength)) |
| expected_counts[base + ".Blocked.ContentLength.ValueIfAvailable"] = 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 |
| << ", expectations=" << expectations; |
| |
| // Determine if the bucket for the resource type (XHR) was incremented. |
| if (0 != (expectations & kShouldBeBlocked)) { |
| 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."; |
| } |
| } |
| |
| // Helper for intercepting a resource request to the given URL and capturing the |
| // response headers and body. |
| // |
| // Note that after the request completes, the original requestor (e.g. the |
| // renderer) will see an injected request failure (this is easier to accomplish |
| // than forwarding the intercepted response to the original requestor), |
| class RequestInterceptor { |
| public: |
| // Start intercepting requests to |url_to_intercept|. |
| explicit RequestInterceptor(const GURL& url_to_intercept) |
| : url_to_intercept_(url_to_intercept), |
| interceptor_( |
| base::BindRepeating(&RequestInterceptor::InterceptorCallback, |
| base::Unretained(this))) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(url_to_intercept.is_valid()); |
| } |
| |
| // Waits until a request gets intercepted and completed. |
| void WaitForRequestCompletion() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(!request_completed_); |
| test_client_.RunUntilComplete(); |
| |
| // Read the intercepted response body into |body_|. |
| if (test_client_.completion_status().error_code == net::OK) { |
| char buffer[128]; |
| while (true) { |
| uint32_t num_bytes = sizeof(buffer); |
| auto result = test_client_.response_body().ReadData( |
| buffer, &num_bytes, MOJO_READ_DATA_FLAG_NONE); |
| if (result != MOJO_RESULT_OK) |
| break; |
| |
| if (num_bytes == 0) |
| break; |
| |
| body_ += std::string(buffer, num_bytes); |
| } |
| } |
| |
| // Wait until IO cleanup completes. |
| base::RunLoop run_loop; |
| BrowserThread::PostTaskAndReply( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&RequestInterceptor::CleanUpOnIOThread, |
| base::Unretained(this)), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // Mark the request as completed (for DCHECK purposes). |
| request_completed_ = true; |
| } |
| |
| const network::URLLoaderCompletionStatus& completion_status() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(request_completed_); |
| return test_client_.completion_status(); |
| } |
| |
| const network::ResourceResponseHead& response_head() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(request_completed_); |
| return test_client_.response_head(); |
| } |
| |
| const std::string& response_body() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(request_completed_); |
| return body_; |
| } |
| |
| private: |
| bool InterceptorCallback(URLLoaderInterceptor::RequestParams* params) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(params); |
| |
| if (url_to_intercept_ != params->url_request.url) |
| return false; |
| |
| // Prevent more than one intercept. |
| if (request_intercepted_) |
| return false; |
| request_intercepted_ = true; |
| |
| // Inject |test_client_| into the request. |
| DCHECK(!original_client_); |
| original_client_ = std::move(params->client); |
| params->client = test_client_.CreateInterfacePtr(); |
| |
| // Forward the request to the original URLLoaderFactory. |
| return false; |
| } |
| |
| void CleanUpOnIOThread() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| // Tell the |original_client_| that the request has completed (and that it |
| // can release its URLLoaderClient. |
| original_client_->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_NOT_IMPLEMENTED)); |
| |
| // Reset all temporary mojo bindings. |
| original_client_.reset(); |
| test_client_.Unbind(); |
| } |
| |
| const GURL url_to_intercept_; |
| URLLoaderInterceptor interceptor_; |
| network::TestURLLoaderClient test_client_; |
| |
| // UI thread state: |
| std::string body_; |
| bool request_completed_ = false; |
| |
| // IO thread state: |
| network::mojom::URLLoaderClientPtr original_client_; |
| bool request_intercepted_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(RequestInterceptor); |
| }; |
| |
| } // namespace |
| |
| // 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(); |
| } |
| |
| 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_blocking/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, |
| kShouldBeSniffedAndBlocked | kShouldHaveContentLength, |
| 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, kShouldBeBlockedWithoutSniffing, 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, kShouldBeSniffedAndAllowed, 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, kShouldBeAllowedWithoutSniffing, 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_blocking/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, kShouldBeBlockedWithoutSniffing | kShouldHaveContentLength, |
| "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, kShouldBeAllowedWithoutSniffing, "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, kShouldBeAllowedWithoutSniffing, "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_blocking/request_target.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo)); |
| |
| // TODO(creis): Wait for all the subresources to load and ensure renderer |
| // process is still alive. |
| } |
| |
| // Checks to see that CORB blocking applies to processes hosting error pages. |
| // Regression test for https://crbug.com/814913. |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingTest, |
| BlockRequestFromErrorPage) { |
| GURL error_url = embedded_test_server()->GetURL("bar.com", "/close-socket"); |
| GURL subresource_url = |
| embedded_test_server()->GetURL("foo.com", "/site_isolation/json.js"); |
| |
| // Load |error_url| and expect a network error page. |
| TestNavigationObserver observer(shell()->web_contents()); |
| EXPECT_FALSE(NavigateToURL(shell(), error_url)); |
| EXPECT_EQ(error_url, observer.last_navigation_url()); |
| NavigationEntry* entry = |
| shell()->web_contents()->GetController().GetLastCommittedEntry(); |
| EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType()); |
| |
| // Add a <script> tag whose src is a CORB-protected resource. Expect no |
| // window.onerror to result, because no syntax error is generated by the empty |
| // response. |
| std::string script = R"((subresource_url => { |
| window.onerror = () => domAutomationController.send("CORB BYPASSED"); |
| var script = document.createElement('script'); |
| script.src = subresource_url; |
| script.onload = () => domAutomationController.send("CORB WORKED"); |
| document.body.appendChild(script); |
| }))"; |
| std::string result; |
| ASSERT_TRUE(ExecuteScriptAndExtractString( |
| shell(), script + "('" + subresource_url.spec() + "')", &result)); |
| |
| EXPECT_EQ("CORB WORKED", result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingTest, BlockHeaders) { |
| GURL foo_url("http://foo.com/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| // Prepare to intercept the network request at the IPC layer. |
| // |
| // Note: we want to verify that the blocking prevents the data from being sent |
| // over IPC. Testing later (e.g. via Response/Headers Web APIs) might give a |
| // false sense of security, since some sanitization happens inside the |
| // renderer (e.g. via FetchResponseData::CreateCORSFilteredResponse). |
| GURL bar_url("http://bar.com/cross_site_document_blocking/headers-test.json"); |
| RequestInterceptor interceptor(bar_url); |
| |
| // Issue the request that will be intercepted |
| EXPECT_TRUE(ExecuteScript(shell(), |
| base::StringPrintf("fetch('%s').catch(error => {})", |
| bar_url.spec().c_str()))); |
| interceptor.WaitForRequestCompletion(); |
| |
| // Verify that the response completed successfully and was blocked. |
| ASSERT_EQ(net::OK, interceptor.completion_status().error_code); |
| ASSERT_TRUE(interceptor.completion_status().blocked_cross_site_document); |
| |
| // Verify that safelisted headers have not been removed by XSDB. |
| // See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name. |
| const std::string& headers = |
| interceptor.response_head().headers->raw_headers(); |
| EXPECT_THAT(headers, |
| HasSubstr("Cache-Control: no-cache, no-store, must-revalidate")); |
| EXPECT_THAT(headers, HasSubstr("Content-Language: TestLanguage")); |
| EXPECT_THAT(headers, |
| HasSubstr("Content-Type: application/json; charset=utf-8")); |
| EXPECT_THAT(headers, HasSubstr("Expires: Wed, 21 Oct 2199 07:28:00 GMT")); |
| EXPECT_THAT(headers, |
| HasSubstr("Last-Modified: Wed, 07 Feb 2018 13:55:00 PST")); |
| EXPECT_THAT(headers, HasSubstr("Pragma: TestPragma")); |
| |
| // Make sure the test covers all the safelisted headers known to the product |
| // code. |
| for (const std::string& safelisted_header : |
| CrossSiteDocumentResourceHandler::GetCorsSafelistedHeadersForTesting()) { |
| EXPECT_TRUE( |
| interceptor.response_head().headers->HasHeader(safelisted_header)); |
| |
| std::string value; |
| interceptor.response_head().headers->EnumerateHeader( |
| nullptr, safelisted_header, &value); |
| EXPECT_FALSE(value.empty()); |
| } |
| |
| // Verify that other response headers have been removed by XSDB. |
| EXPECT_THAT(headers, Not(HasSubstr("Content-Length"))); |
| EXPECT_THAT(headers, Not(HasSubstr("X-My-Secret-Header"))); |
| EXPECT_THAT(headers, Not(HasSubstr("MySecretCookieKey"))); |
| EXPECT_THAT(headers, Not(HasSubstr("MySecretCookieValue"))); |
| |
| // Verify that the body is empty. |
| EXPECT_EQ("", interceptor.response_body()); |
| EXPECT_EQ(0, interceptor.completion_status().decoded_body_length); |
| |
| // Verify that other response parts have been sanitized. |
| EXPECT_EQ(0u, interceptor.response_head().content_length); |
| } |
| |
| // This test class sets up a service worker that can be used to try to respond |
| // to same-origin requests with cross-origin responses. |
| class CrossSiteDocumentBlockingServiceWorkerTest : public ContentBrowserTest { |
| public: |
| CrossSiteDocumentBlockingServiceWorkerTest() |
| : service_worker_https_server_(net::EmbeddedTestServer::TYPE_HTTPS), |
| cross_origin_https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} |
| ~CrossSiteDocumentBlockingServiceWorkerTest() override {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| IsolateAllSitesForTesting(command_line); |
| |
| // 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); |
| |
| ContentBrowserTest::SetUpCommandLine(command_line); |
| } |
| |
| void SetUpOnMainThread() override { |
| SetupCrossSiteRedirector(embedded_test_server()); |
| |
| service_worker_https_server_.ServeFilesFromSourceDirectory( |
| "content/test/data"); |
| ASSERT_TRUE(service_worker_https_server_.Start()); |
| |
| cross_origin_https_server_.ServeFilesFromSourceDirectory( |
| "content/test/data"); |
| cross_origin_https_server_.SetSSLConfig( |
| net::EmbeddedTestServer::CERT_COMMON_NAME_IS_DOMAIN); |
| ASSERT_TRUE(cross_origin_https_server_.Start()); |
| |
| // Sanity check of test setup - the 2 https servers should be cross-site |
| // (the second server should have a different hostname because of the call |
| // to SetSSLConfig with CERT_COMMON_NAME_IS_DOMAIN argument). |
| ASSERT_FALSE(SiteInstance::IsSameWebSite( |
| shell()->web_contents()->GetBrowserContext(), |
| GetURLOnServiceWorkerServer("/"), GetURLOnCrossOriginServer("/"))); |
| } |
| |
| GURL GetURLOnServiceWorkerServer(const std::string& path) { |
| return service_worker_https_server_.GetURL(path); |
| } |
| |
| GURL GetURLOnCrossOriginServer(const std::string& path) { |
| return cross_origin_https_server_.GetURL(path); |
| } |
| |
| void StopCrossOriginServer() { |
| EXPECT_TRUE(cross_origin_https_server_.ShutdownAndWaitUntilComplete()); |
| } |
| |
| void SetUpServiceWorker() { |
| GURL url = GetURLOnServiceWorkerServer( |
| "/cross_site_document_blocking/request.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Register the service worker. |
| bool is_script_done; |
| std::string script = R"( |
| navigator.serviceWorker |
| .register('/cross_site_document_blocking/service_worker.js') |
| .then(registration => navigator.serviceWorker.ready) |
| .then(function(r) { domAutomationController.send(true); }) |
| .catch(function(e) { |
| console.log('error: ' + e); |
| domAutomationController.send(false); |
| }); )"; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool(shell(), script, &is_script_done)); |
| ASSERT_TRUE(is_script_done); |
| |
| // Navigate again to the same URL - the service worker should be 1) active |
| // at this time (because of waiting for |navigator.serviceWorker.ready| |
| // above) and 2) controlling the current page (because of the reload). |
| ASSERT_TRUE(NavigateToURL(shell(), url)); |
| bool is_controlled_by_service_worker; |
| ASSERT_TRUE(ExecuteScriptAndExtractBool( |
| shell(), |
| "domAutomationController.send(!!navigator.serviceWorker.controller)", |
| &is_controlled_by_service_worker)); |
| ASSERT_TRUE(is_controlled_by_service_worker); |
| } |
| |
| private: |
| // The test requires 2 https servers, because: |
| // 1. Service workers are only supported on secure origins. |
| // 2. One of tests requires fetching cross-origin resources from the |
| // original page and/or service worker - the target of the fetch needs to |
| // be a https server to avoid hitting the mixed content error. |
| net::EmbeddedTestServer service_worker_https_server_; |
| net::EmbeddedTestServer cross_origin_https_server_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentBlockingServiceWorkerTest); |
| }; |
| |
| // Issue a cross-origin request that will be handled entirely within a service |
| // worker (without reaching the network - the cross-origin response will be |
| // "faked" within the same-origin service worker, because the service worker |
| // used by the test recognizes the "data_from_service_worker" suffix in the |
| // URL). This testcase is designed to hit the case in |
| // CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders where |
| // |response_type_via_service_worker| is equal to |kDefault|. See also |
| // https://crbug.com/803672. |
| // |
| // TODO(lukasza): https://crbug.com/715640: This test might become invalid |
| // after servicification of service workers. |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingServiceWorkerTest, NoNetwork) { |
| SetUpServiceWorker(); |
| |
| base::HistogramTester histograms; |
| std::string response; |
| std::string script = R"( |
| // Any cross-origin URL ending with .../data_from_service_worker can be |
| // used here - it will be intercepted by the service worker and will never |
| // go to the network. |
| fetch('https://bar.com/data_from_service_worker') |
| .then(response => response.text()) |
| .then(responseText => { |
| domAutomationController.send(responseText); |
| }) |
| .catch(error => { |
| var errorMessage = 'error: ' + error; |
| console.log(errorMessage); |
| domAutomationController.send(errorMessage); |
| }); )"; |
| EXPECT_TRUE(ExecuteScriptAndExtractString(shell(), script, &response)); |
| |
| // Verify that XSDB didn't block the response (since it was "faked" within the |
| // service worker and didn't cross any security boundaries). |
| EXPECT_EQ("Response created by service worker", response); |
| InspectHistograms(histograms, kShouldBeAllowedWithoutSniffing, "blah.html", |
| RESOURCE_TYPE_XHR); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CrossSiteDocumentBlockingServiceWorkerTest, |
| NetworkToServiceWorkerResponse) { |
| SetUpServiceWorker(); |
| |
| // Build a script for XHR-ing a cross-origin, nosniff HTML document. |
| GURL cross_origin_url = |
| GetURLOnCrossOriginServer("/site_isolation/nosniff.txt"); |
| const char* script_template = R"( |
| fetch('%s', { mode: 'no-cors' }) |
| .then(response => response.text()) |
| .then(responseText => { |
| domAutomationController.send(responseText); |
| }) |
| .catch(error => { |
| var errorMessage = 'error: ' + error; |
| domAutomationController.send(errorMessage); |
| }); )"; |
| std::string script = |
| base::StringPrintf(script_template, cross_origin_url.spec().c_str()); |
| |
| // The service worker will forward the request to the network, but a response |
| // will be intercepted by the service worker and replaced with a new, |
| // artificial error. |
| base::HistogramTester histograms; |
| std::string response; |
| EXPECT_TRUE(ExecuteScriptAndExtractString(shell(), script, &response)); |
| |
| // Verify that XSDB blocked the response from the network (from |
| // |cross_origin_https_server_|) to the service worker. |
| InspectHistograms(histograms, kShouldBeBlockedWithoutSniffing, "network.txt", |
| RESOURCE_TYPE_XHR); |
| |
| // Verify that the service worker replied with an expected error. |
| // Replying with an error means that XSDB is only active once (for the |
| // initial, real network request) and therefore the test doesn't get |
| // confused (second successful response would have added noise to the |
| // histograms captured by the test). |
| EXPECT_EQ("error: TypeError: Failed to fetch", response); |
| } |
| |
| 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_blocking/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_blocking/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_blocking/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 |