diff options
Diffstat (limited to 'chromium/content/browser/loader')
8 files changed, 1826 insertions, 18 deletions
diff --git a/chromium/content/browser/loader/DEPS b/chromium/content/browser/loader/DEPS index 03317bf0cc9..194a58803b2 100644 --- a/chromium/content/browser/loader/DEPS +++ b/chromium/content/browser/loader/DEPS @@ -111,6 +111,7 @@ specific_include_rules = { "resource_dispatcher_host_impl\.(cc|h)": [ "-content", "+content/browser/loader/async_resource_handler.h", + "+content/browser/loader/cross_site_document_resource_handler.h", "+content/browser/loader/global_routing_id.h", "+content/browser/loader/loader_delegate.h", "+content/browser/loader/mojo_async_resource_handler.h", diff --git a/chromium/content/browser/loader/cross_site_document_blocking_browsertest.cc b/chromium/content/browser/loader/cross_site_document_blocking_browsertest.cc new file mode 100644 index 00000000000..101bc198bee --- /dev/null +++ b/chromium/content/browser/loader/cross_site_document_blocking_browsertest.cc @@ -0,0 +1,380 @@ +// 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 "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( + 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 { + EXPECT_FALSE(should_be_blocked); + bucket = "Other"; + } + + // 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 (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. + 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"}; + 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. + 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"}; + 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. + // valid.* - Correctly labeled responses of non-document types. + const char* allowed_resources[] = {"cors.html", "cors.xml", "cors.json", + "cors.txt", "valid.js"}; + 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 diff --git a/chromium/content/browser/loader/cross_site_document_resource_handler.cc b/chromium/content/browser/loader/cross_site_document_resource_handler.cc new file mode 100644 index 00000000000..475ead7f3af --- /dev/null +++ b/chromium/content/browser/loader/cross_site_document_resource_handler.cc @@ -0,0 +1,420 @@ +// 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 "content/browser/loader/cross_site_document_resource_handler.h" + +#include <string.h> +#include <memory> +#include <string> +#include <utility> + +#include "base/logging.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_piece.h" +#include "base/trace_event/trace_event.h" +#include "content/browser/child_process_security_policy_impl.h" +#include "content/browser/loader/detachable_resource_handler.h" +#include "content/browser/loader/resource_request_info_impl.h" +#include "content/browser/site_instance_impl.h" +#include "content/common/site_isolation_policy.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/resource_context.h" +#include "content/public/common/content_client.h" +#include "net/base/io_buffer.h" +#include "net/base/mime_sniffer.h" +#include "net/url_request/url_request.h" + +namespace content { + +namespace { + +void LogCrossSiteDocumentAction( + CrossSiteDocumentResourceHandler::Action action) { + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Action", action, + CrossSiteDocumentResourceHandler::Action::kCount); +} + +} // namespace + +// ResourceController used in OnWillRead in cases that sniffing will happen. +// When invoked, it runs the corresponding method on the ResourceHandler. +class CrossSiteDocumentResourceHandler::OnWillReadController + : public ResourceController { + public: + // Keeps track of the addresses of the ResourceLoader's buffer and size, + // which will be populated by the downstream ResourceHandler by the time that + // Resume() is called. + explicit OnWillReadController( + CrossSiteDocumentResourceHandler* document_handler, + scoped_refptr<net::IOBuffer>* buf, + int* buf_size) + : document_handler_(document_handler), buf_(buf), buf_size_(buf_size) {} + + ~OnWillReadController() override {} + + // ResourceController implementation: + void Resume() override { + MarkAsUsed(); + + // Now that |buf_| has a buffer written into it by the downstream handler, + // set up sniffing in the CrossSiteDocumentResourceHandler. + document_handler_->ResumeOnWillRead(buf_, buf_size_); + } + + void Cancel() override { + MarkAsUsed(); + document_handler_->Cancel(); + } + + void CancelWithError(int error_code) override { + MarkAsUsed(); + document_handler_->CancelWithError(error_code); + } + + private: + void MarkAsUsed() { +#if DCHECK_IS_ON() + DCHECK(!used_); + used_ = true; +#endif + } + +#if DCHECK_IS_ON() + bool used_ = false; +#endif + + CrossSiteDocumentResourceHandler* document_handler_; + + // Address of the ResourceLoader's buffer, which will be populated by the + // downstream handler before Resume() is called. + scoped_refptr<net::IOBuffer>* buf_; + + // Address of the size of |buf_|, similarly populated downstream. + int* buf_size_; + + DISALLOW_COPY_AND_ASSIGN(OnWillReadController); +}; + +CrossSiteDocumentResourceHandler::CrossSiteDocumentResourceHandler( + std::unique_ptr<ResourceHandler> next_handler, + net::URLRequest* request, + bool is_nocors_plugin_request) + : LayeredResourceHandler(request, std::move(next_handler)), + is_nocors_plugin_request_(is_nocors_plugin_request) {} + +CrossSiteDocumentResourceHandler::~CrossSiteDocumentResourceHandler() {} + +void CrossSiteDocumentResourceHandler::OnResponseStarted( + ResourceResponse* response, + std::unique_ptr<ResourceController> controller) { + has_response_started_ = true; + LogCrossSiteDocumentAction( + CrossSiteDocumentResourceHandler::Action::kResponseStarted); + + should_block_based_on_headers_ = ShouldBlockBasedOnHeaders(response); + next_handler_->OnResponseStarted(response, std::move(controller)); +} + +void CrossSiteDocumentResourceHandler::OnWillRead( + scoped_refptr<net::IOBuffer>* buf, + int* buf_size, + std::unique_ptr<ResourceController> controller) { + DCHECK(has_response_started_); + + // On the next read attempt after the response was blocked, either cancel the + // rest of the request or allow it to proceed in a detached state. + if (blocked_read_completed_) { + DCHECK(should_block_based_on_headers_); + DCHECK(!allow_based_on_sniffing_); + const ResourceRequestInfoImpl* info = GetRequestInfo(); + if (info && info->detachable_handler()) { + // Ensure that prefetch, etc, continue to cache the response, without + // sending it to the renderer. + info->detachable_handler()->Detach(); + } else { + // If it's not detachable, cancel the rest of the request. + controller->Cancel(); + } + return; + } + + // If we intended to block the response and haven't yet decided to allow it + // due to sniffing, we will read some of the data to a local buffer to sniff + // it. Since the downstream handler may defer during the OnWillRead call + // below, the values of |buf| and |buf_size| may not be available right away. + // Instead, create an OnWillReadController to start the sniffing after the + // downstream handler has called Resume on it. + if (should_block_based_on_headers_ && !allow_based_on_sniffing_) { + HoldController(std::move(controller)); + controller = std::make_unique<OnWillReadController>(this, buf, buf_size); + } + + // Have the downstream handler(s) allocate the real buffer to use. + next_handler_->OnWillRead(buf, buf_size, std::move(controller)); +} + +void CrossSiteDocumentResourceHandler::ResumeOnWillRead( + scoped_refptr<net::IOBuffer>* buf, + int* buf_size) { + // We should only get here in cases that we intend to sniff the data, after + // downstream handler finishes its work from OnWillRead. + DCHECK(should_block_based_on_headers_); + DCHECK(!allow_based_on_sniffing_); + DCHECK(!blocked_read_completed_); + + // For most blocked responses, we need to sniff the data to confirm it looks + // like the claimed MIME type (to avoid blocking mislabeled JavaScript, + // JSONP, etc). Read this data into a separate buffer (not shared with the + // renderer), which we will only copy over if we decide to allow it through. + // This is only done when we suspect the response should be blocked. + // + // Make it as big as the downstream handler's buffer to make it easy to copy + // over in one operation. This will be large, since the MIME sniffing + // handler is downstream. Technically we could use a smaller buffer if + // |needs_sniffing_| is false, but there's no need for the extra complexity. + DCHECK_GE(*buf_size, net::kMaxBytesToSniff); + local_buffer_ = + base::MakeRefCounted<net::IOBuffer>(static_cast<size_t>(*buf_size)); + + // Store the next handler's buffer but don't read into it while sniffing, + // since we probably won't want to send the data to the renderer process. + next_handler_buffer_ = *buf; + next_handler_buffer_size_ = *buf_size; + *buf = local_buffer_; + + Resume(); +} + +void CrossSiteDocumentResourceHandler::OnReadCompleted( + int bytes_read, + std::unique_ptr<ResourceController> controller) { + DCHECK(has_response_started_); + DCHECK(!blocked_read_completed_); + + // If we intended to block the response and haven't sniffed yet, try to + // confirm that we should block it. If sniffing is needed, look at the local + // buffer and either report that zero bytes were read (to indicate the + // response is empty and complete), or copy the sniffed data to the next + // handler's buffer and resume the response without blocking. + if (should_block_based_on_headers_ && !allow_based_on_sniffing_) { + bool confirmed_blockable = false; + if (!needs_sniffing_) { + // TODO(creis): Also consider the MIME type confirmed if |bytes_read| is + // too small to do sniffing, or restructure to allow buffering enough. + // For now, responses with small initial reads may be allowed through. + confirmed_blockable = true; + } else { + // Sniff the data to see if it likely matches the MIME type that caused us + // to decide to block it. If it doesn't match, it may be JavaScript, + // JSONP, or another allowable data type and we should let it through. + // Record how many bytes were read to see how often it's too small. (This + // will typically be under 100,000.) + UMA_HISTOGRAM_COUNTS("SiteIsolation.XSD.Browser.BytesReadForSniffing", + bytes_read); + DCHECK_LE(bytes_read, next_handler_buffer_size_); + base::StringPiece data(local_buffer_->data(), bytes_read); + + // Confirm whether the data is HTML, XML, or JSON. + if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_HTML) { + confirmed_blockable = CrossSiteDocumentClassifier::SniffForHTML(data); + } else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_XML) { + confirmed_blockable = CrossSiteDocumentClassifier::SniffForXML(data); + } else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_JSON) { + confirmed_blockable = CrossSiteDocumentClassifier::SniffForJSON(data); + } else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN) { + // For responses labeled as plain text, only block them if the data + // sniffs as one of the formats we would block in the first place. + confirmed_blockable = CrossSiteDocumentClassifier::SniffForHTML(data) || + CrossSiteDocumentClassifier::SniffForXML(data) || + CrossSiteDocumentClassifier::SniffForJSON(data); + } + } + + if (confirmed_blockable) { + // Block the response and throw away the data. Report zero bytes read. + bytes_read = 0; + blocked_read_completed_ = true; + + // Log the blocking event. Inline the Serialize call to avoid it when + // tracing is disabled. + TRACE_EVENT2("navigation", + "CrossSiteDocumentResourceHandler::ShouldBlockResponse", + "initiator", + request()->initiator().has_value() + ? request()->initiator().value().Serialize() + : "null", + "url", request()->url().spec()); + + LogCrossSiteDocumentAction( + needs_sniffing_ + ? CrossSiteDocumentResourceHandler::Action::kBlockedAfterSniffing + : CrossSiteDocumentResourceHandler::Action:: + kBlockedWithoutSniffing); + ResourceType resource_type = GetRequestInfo()->GetResourceType(); + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked", + resource_type, + content::RESOURCE_TYPE_LAST_TYPE); + switch (canonical_mime_type_) { + case CROSS_SITE_DOCUMENT_MIME_TYPE_HTML: + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.HTML", + resource_type, + content::RESOURCE_TYPE_LAST_TYPE); + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_XML: + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.XML", + resource_type, + content::RESOURCE_TYPE_LAST_TYPE); + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_JSON: + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.JSON", + resource_type, + content::RESOURCE_TYPE_LAST_TYPE); + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN: + UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Plain", + resource_type, + content::RESOURCE_TYPE_LAST_TYPE); + break; + default: + NOTREACHED(); + } + } else { + // Allow the response through instead and proceed with reading more. + // Copy sniffed data into the next handler's buffer before proceeding. + // Note that the size of the two buffers is the same (see OnWillRead). + DCHECK_LE(bytes_read, next_handler_buffer_size_); + memcpy(next_handler_buffer_->data(), local_buffer_->data(), bytes_read); + allow_based_on_sniffing_ = true; + } + + // Clean up, whether we'll cancel or proceed from here. + local_buffer_ = nullptr; + next_handler_buffer_ = nullptr; + next_handler_buffer_size_ = 0; + } + + next_handler_->OnReadCompleted(bytes_read, std::move(controller)); +} + +void CrossSiteDocumentResourceHandler::OnResponseCompleted( + const net::URLRequestStatus& status, + std::unique_ptr<ResourceController> controller) { + if (blocked_read_completed_) { + // Report blocked responses as successful, rather than the cancellation + // from OnWillRead. + next_handler_->OnResponseCompleted(net::URLRequestStatus(), + std::move(controller)); + } else { + LogCrossSiteDocumentAction( + needs_sniffing_ + ? CrossSiteDocumentResourceHandler::Action::kAllowedAfterSniffing + : CrossSiteDocumentResourceHandler::Action:: + kAllowedWithoutSniffing); + + next_handler_->OnResponseCompleted(status, std::move(controller)); + } +} + +bool CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders( + ResourceResponse* response) { + // The checks in this method are ordered to rule out blocking in most cases as + // quickly as possible. Checks that are likely to lead to returning false or + // that are inexpensive should be near the top. + const GURL& url = request()->url(); + + // Check if the response's site needs to have its documents protected. By + // default, this will usually return false. + // TODO(creis): This check can go away once the logic here is made fully + // backward compatible and we can enforce it always, regardless of Site + // Isolation policy. + switch (SiteIsolationPolicy::IsCrossSiteDocumentBlockingEnabled()) { + case SiteIsolationPolicy::XSDB_ENABLED_UNCONDITIONALLY: + break; + case SiteIsolationPolicy::XSDB_ENABLED_IF_ISOLATED: + if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites() && + !ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedOrigin( + url::Origin::Create(url))) { + return false; + } + break; + case SiteIsolationPolicy::XSDB_DISABLED: + return false; + } + + // Look up MIME type. If it doesn't claim to be a blockable type (i.e., HTML, + // XML, JSON, or plain text), don't block it. + canonical_mime_type_ = CrossSiteDocumentClassifier::GetCanonicalMimeType( + response->head.mime_type); + if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS) + return false; + + // Treat a missing initiator as an empty origin to be safe, though we don't + // expect this to happen. Unfortunately, this requires a copy. + url::Origin initiator; + if (request()->initiator().has_value()) + initiator = request()->initiator().value(); + + // Don't block same-site documents. + if (CrossSiteDocumentClassifier::IsSameSite(initiator, url)) + return false; + + // Only block documents from HTTP(S) schemes. + if (!CrossSiteDocumentClassifier::IsBlockableScheme(url)) + return false; + + // Allow requests from file:// URLs for now. + // TODO(creis): Limit this to when the allow_universal_access_from_file_urls + // preference is set. See https://crbug.com/789781. + if (initiator.scheme() == url::kFileScheme) + return false; + + // Only block if this is a request made from a renderer process. + const ResourceRequestInfoImpl* info = GetRequestInfo(); + if (!info || info->GetChildID() == -1) + return false; + + // Give embedder a chance to skip document blocking for this response. + if (GetContentClient()->browser()->ShouldBypassDocumentBlocking( + initiator, url, info->GetResourceType())) { + return false; + } + + // Allow the response through if it has valid CORS headers. + std::string cors_header; + response->head.headers->GetNormalizedHeader("access-control-allow-origin", + &cors_header); + if (CrossSiteDocumentClassifier::IsValidCorsHeaderSet(initiator, url, + cors_header)) { + return false; + } + + // Don't block plugin requests with universal access (e.g., Flash). Such + // requests are made without CORS, and thus dont have an Origin request + // header. Other plugin requests (e.g., NaCl) are made using CORS and have an + // Origin request header. If they fail the CORS check above, they should be + // blocked. + if (info->GetResourceType() == RESOURCE_TYPE_PLUGIN_RESOURCE && + is_nocors_plugin_request_) { + return false; + } + + // We intend to block the response at this point. However, we will usually + // sniff the contents to confirm the MIME type, to avoid blocking incorrectly + // labeled JavaScript, JSONP, etc files. + // + // Note: only sniff if there isn't a nosniff header, and if it is not a range + // request. Range requests would let an attacker bypass blocking by + // requesting a range that fails to sniff as a protected type. + std::string nosniff_header; + response->head.headers->GetNormalizedHeader("x-content-type-options", + &nosniff_header); + std::string range_header; + response->head.headers->GetNormalizedHeader("content-range", &range_header); + needs_sniffing_ = !base::LowerCaseEqualsASCII(nosniff_header, "nosniff") && + range_header.empty(); + + return true; +} + +} // namespace content diff --git a/chromium/content/browser/loader/cross_site_document_resource_handler.h b/chromium/content/browser/loader/cross_site_document_resource_handler.h new file mode 100644 index 00000000000..4dd1d4a89b5 --- /dev/null +++ b/chromium/content/browser/loader/cross_site_document_resource_handler.h @@ -0,0 +1,156 @@ +// 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. + +#ifndef CONTENT_BROWSER_LOADER_CROSS_SITE_DOCUMENT_RESOURCE_HANDLER_H_ +#define CONTENT_BROWSER_LOADER_CROSS_SITE_DOCUMENT_RESOURCE_HANDLER_H_ + +#include <memory> + +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "content/browser/loader/layered_resource_handler.h" +#include "content/common/cross_site_document_classifier.h" +#include "content/public/common/resource_type.h" + +namespace net { +class URLRequest; +} // namespace net + +namespace content { + +// A ResourceHandler that prevents the renderer process from receiving network +// responses that contain cross-site documents (HTML, XML, some plain text) or +// similar data that should be opaque (JSON), with appropriate exceptions to +// preserve compatibility. Other cross-site resources such as scripts, images, +// stylesheets, etc are still allowed. +// +// This handler is not used for navigations, which create a new security context +// based on the origin of the response. It currently only protects documents +// from sites that require dedicated renderer processes, though it could be +// expanded to apply to all sites. +// +// When a response is blocked, the renderer is sent an empty response body +// instead of seeing a failed request. A failed request would change page- +// visible behavior (e.g., for a blocked XHR). An empty response can generally +// be consumed by the renderer without noticing the difference. +// +// For more details, see: +// http://chromium.org/developers/design-documents/blocking-cross-site-documents +class CONTENT_EXPORT CrossSiteDocumentResourceHandler + : public LayeredResourceHandler { + public: + // This enum backs a histogram. Update enums.xml if you make any updates, and + // put new entries before |kCount|. + enum class Action { + // Logged at OnResponseStarted. + kResponseStarted, + + // Logged when a response is blocked without requiring sniffing. + kBlockedWithoutSniffing, + + // Logged when a response is blocked as a result of sniffing the content. + kBlockedAfterSniffing, + + // Logged when a response is allowed without requiring sniffing. + kAllowedWithoutSniffing, + + // Logged when a response is allowed as a result of sniffing the content. + kAllowedAfterSniffing, + + kCount + }; + + CrossSiteDocumentResourceHandler( + std::unique_ptr<ResourceHandler> next_handler, + net::URLRequest* request, + bool is_nocors_plugin_request); + ~CrossSiteDocumentResourceHandler() override; + + // LayeredResourceHandler overrides: + void OnResponseStarted( + ResourceResponse* response, + std::unique_ptr<ResourceController> controller) override; + void OnWillRead(scoped_refptr<net::IOBuffer>* buf, + int* buf_size, + std::unique_ptr<ResourceController> controller) override; + void OnReadCompleted(int bytes_read, + std::unique_ptr<ResourceController> controller) override; + void OnResponseCompleted( + const net::URLRequestStatus& status, + std::unique_ptr<ResourceController> controller) override; + + private: + FRIEND_TEST_ALL_PREFIXES(CrossSiteDocumentResourceHandlerTest, + ResponseBlocking); + FRIEND_TEST_ALL_PREFIXES(CrossSiteDocumentResourceHandlerTest, + OnWillReadDefer); + + // ResourceController that manages the read buffer if a downstream handler + // defers during OnWillRead. + class OnWillReadController; + + // Computes whether this response contains a cross-site document that needs to + // be blocked from the renderer process. This is a first approximation based + // on the headers, and may be revised after some of the data is sniffed. + bool ShouldBlockBasedOnHeaders(ResourceResponse* response); + + // Once the downstream handler has allocated the buffer for OnWillRead + // (possibly after deferring), this sets up sniffing into a local buffer. + // Called by the OnWillReadController. + void ResumeOnWillRead(scoped_refptr<net::IOBuffer>* buf, int* buf_size); + + // A local buffer for sniffing content and using for throwaway reads. + // This is not shared with the renderer process. + scoped_refptr<net::IOBuffer> local_buffer_; + + // The buffer allocated by the next ResourceHandler for reads, which is used + // if sniffing determines that we should proceed with the response. + scoped_refptr<net::IOBuffer> next_handler_buffer_; + + // The size of |next_handler_buffer_|. + int next_handler_buffer_size_ = 0; + + // A canonicalization of the specified MIME type, to determine if blocking the + // response is needed, as well as which type of sniffing to perform. + CrossSiteDocumentMimeType canonical_mime_type_ = + CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS; + + // Indicates whether this request was made by a plugin and was not using CORS. + // Such requests are exempt from blocking, while other plugin requests must be + // blocked if the CORS check fails. + // TODO(creis, nick): Replace this with a plugin process ID check to see if + // the plugin has universal access. + bool is_nocors_plugin_request_; + + // Tracks whether OnResponseStarted has been called, to ensure that it happens + // before OnWillRead and OnReadCompleted. + bool has_response_started_ = false; + + // Whether this response is a cross-site document that should be blocked, + // pending the outcome of sniffing the content. Set in OnResponseStarted and + // should only be read afterwards. + bool should_block_based_on_headers_ = false; + + // Whether the response data should be sniffed before blocking it, to avoid + // blocking mislabeled responses (e.g., JSONP labeled as HTML). This is + // usually true when |should_block_based_on_headers_| is set, unless there is + // a nosniff header or range request. + bool needs_sniffing_ = false; + + // Whether this response will be allowed through despite being flagged for + // blocking (via |should_block_based_on_headers_), because sniffing determined + // it was incorrectly labeled and might be needed for compatibility (e.g., + // in case it is Javascript). + bool allow_based_on_sniffing_ = false; + + // Whether the next ResourceHandler has already been told that the read has + // completed, and thus it is safe to cancel or detach on the next read. + bool blocked_read_completed_ = false; + + DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentResourceHandler); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_LOADER_CROSS_SITE_DOCUMENT_RESOURCE_HANDLER_H_ diff --git a/chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc b/chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc new file mode 100644 index 00000000000..ff3f1423c00 --- /dev/null +++ b/chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc @@ -0,0 +1,836 @@ +// 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 "content/browser/loader/cross_site_document_resource_handler.h" + +#include <stdint.h> + +#include <memory> +#include <string> +#include <utility> + +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" +#include "base/test/histogram_tester.h" +#include "base/threading/thread_task_runner_handle.h" +#include "content/browser/loader/mock_resource_loader.h" +#include "content/browser/loader/resource_controller.h" +#include "content/browser/loader/test_resource_handler.h" +#include "content/public/browser/resource_request_info.h" +#include "content/public/common/resource_response.h" +#include "content/public/common/resource_type.h" +#include "content/public/common/webplugininfo.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "content/public/test/test_utils.h" +#include "net/base/net_errors.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace content { + +namespace { + +enum class OriginHeader { kOmit, kInclude }; + +enum class AccessControlAllowOriginHeader { + kOmit, + kAllowAny, + kAllowNull, + kAllowInitiatorOrigin, + kAllowExampleDotCom +}; + +enum class Verdict { + kAllowWithoutSniffing, + kBlockWithoutSniffing, + kAllowAfterSniffing, + kBlockAfterSniffing +}; + +// This struct is used to describe each test case in this file. It's passed as +// a test parameter to each TEST_P test. +struct TestScenario { + // Attributes to make test failure messages useful. + const char* description; + int source_line; + + // Attributes of the HTTP Request. + const char* target_url; + ResourceType resource_type; + const char* initiator_origin; + OriginHeader cors_request; + + // Attributes of the HTTP response. + const char* response_mime_type; + CrossSiteDocumentMimeType canonical_mime_type; + bool include_no_sniff_header; + AccessControlAllowOriginHeader cors_response; + const char* first_chunk; + + // Expected result. + Verdict verdict; +}; + +// Stream operator to let GetParam() print a useful result if any tests fail. +::std::ostream& operator<<(::std::ostream& os, const TestScenario& scenario) { + std::string cors_response; + switch (scenario.cors_response) { + case AccessControlAllowOriginHeader::kOmit: + cors_response = "AccessControlAllowOriginHeader::kOmit"; + break; + case AccessControlAllowOriginHeader::kAllowAny: + cors_response = "AccessControlAllowOriginHeader::kAllowAny"; + break; + case AccessControlAllowOriginHeader::kAllowNull: + cors_response = "AccessControlAllowOriginHeader::kAllowNull"; + break; + case AccessControlAllowOriginHeader::kAllowInitiatorOrigin: + cors_response = "AccessControlAllowOriginHeader::kAllowInitiatorOrigin"; + break; + case AccessControlAllowOriginHeader::kAllowExampleDotCom: + cors_response = "AccessControlAllowOriginHeader::kAllowExampleDotCom"; + break; + default: + NOTREACHED(); + } + + std::string verdict; + switch (scenario.verdict) { + case Verdict::kAllowWithoutSniffing: + verdict = "Verdict::kAllowWithoutSniffing"; + break; + case Verdict::kBlockWithoutSniffing: + verdict = "Verdict::kBlockWithoutSniffing"; + break; + case Verdict::kAllowAfterSniffing: + verdict = "Verdict::kAllowAfterSniffing"; + break; + case Verdict::kBlockAfterSniffing: + verdict = "Verdict::kBlockAfterSniffing"; + break; + default: + NOTREACHED(); + } + + return os << "\n description = " << scenario.description + << "\n target_url = " << scenario.target_url + << "\n resource_type = " << scenario.resource_type + << "\n initiator_origin = " << scenario.initiator_origin + << "\n cors_request = " + << (scenario.cors_request == OriginHeader::kOmit + ? "OriginHeader::kOmit" + : "OriginHeader::kInclude") + << "\n response_mime_type = " << scenario.response_mime_type + << "\n canonical_mime_type = " << scenario.canonical_mime_type + << "\n include_no_sniff = " + << (scenario.include_no_sniff_header ? "true" : "false") + << "\n cors_response = " << cors_response + << "\n first_chunk = " << scenario.first_chunk + << "\n verdict = " << verdict; +} + +// A set of test cases that verify CrossSiteDocumentResourceHandler correctly +// classifies network responses as allowed or blocked. These TestScenarios are +// passed to the TEST_P tests below as test parameters. +const TestScenario kScenarios[] = { + // Allowed responses: + { + "Allowed: Same-site XHR to HTML", __LINE__, + "http://www.a.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site script", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_SCRIPT, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "application/javascript", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "var x=3;", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to HTML with CORS for origin", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kAllowInitiatorOrigin, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to XML with CORS for any", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "application/rss+xml", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_XML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kAllowAny, // cors_response + "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to JSON with CORS for null", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "text/json", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_JSON, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kAllowNull, // cors_response + "{\"x\" : 3}", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to HTML over FTP", __LINE__, + "ftp://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to HTML from file://", __LINE__, + "file:///foo/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site fetch HTML from Flash without CORS", __LINE__, + "http://www.b.com/plugin.html", // target_url + RESOURCE_TYPE_PLUGIN_RESOURCE, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + { + "Allowed: Cross-site fetch HTML from NaCl with CORS response", __LINE__, + "http://www.b.com/plugin.html", // target_url + RESOURCE_TYPE_PLUGIN_RESOURCE, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kAllowInitiatorOrigin, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kAllowWithoutSniffing, // verdict + }, + + // Allowed responses due to sniffing: + { + "Allowed: Cross-site script to JSONP labeled as HTML", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_SCRIPT, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "foo({\"x\" : 3})", // first_chunk + Verdict::kAllowAfterSniffing, // verdict + }, + { + "Allowed: Cross-site script to JavaScript labeled as text", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_SCRIPT, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/plain", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "var x = 3;", // first_chunk + Verdict::kAllowAfterSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to nonsense labeled as XML", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "application/xml", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_XML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "Won't sniff as XML", // first_chunk + Verdict::kAllowAfterSniffing, // verdict + }, + { + "Allowed: Cross-site XHR to nonsense labeled as JSON", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/x-json", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_JSON, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "Won't sniff as JSON", // first_chunk + Verdict::kAllowAfterSniffing, // verdict + }, + // TODO(creis): We should block the following response since there isn't + // enough data to confirm it as HTML by sniffing. + { + "Allowed for now: Cross-site XHR to HTML with small first read", + __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<htm", // first_chunk + Verdict::kAllowAfterSniffing, // verdict + }, + + // Blocked responses: + { + "Blocked: Cross-site XHR to HTML without CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to XML without CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "application/xml", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_XML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to JSON without CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "application/json", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_JSON, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "{\"x\" : 3}", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to HTML labeled as text without CORS", + __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/plain", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to nosniff HTML without CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + true, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kBlockWithoutSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to nosniff response without CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + true, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "Wouldn't sniff as HTML", // first_chunk + Verdict::kBlockWithoutSniffing, // verdict + }, + { + "Blocked: Cross-site <script> inclusion of HTML w/ DTD without CORS", + __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_SCRIPT, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kOmit, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<!doctype html><html itemscope=\"\" " + "itemtype=\"http://schema.org/SearchResultsPage\" " + "lang=\"en\"><head>", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site XHR to HTML with wrong CORS", __LINE__, + "http://www.b.com/resource.html", // target_url + RESOURCE_TYPE_XHR, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kAllowExampleDotCom, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, + { + "Blocked: Cross-site fetch HTML from NaCl without CORS response", + __LINE__, + "http://www.b.com/plugin.html", // target_url + RESOURCE_TYPE_PLUGIN_RESOURCE, // resource_type + "http://www.a.com/", // initiator_origin + OriginHeader::kInclude, // cors_request + "text/html", // response_mime_type + CROSS_SITE_DOCUMENT_MIME_TYPE_HTML, // canonical_mime_type + false, // include_no_sniff_header + AccessControlAllowOriginHeader::kOmit, // cors_response + "<html><head>this should sniff as HTML", // first_chunk + Verdict::kBlockAfterSniffing, // verdict + }, +}; + +} // namespace + +// Tests that verify CrossSiteDocumentResourceHandler correctly classifies +// network responses as allowed or blocked, and ensures that empty responses are +// sent for the blocked cases. +// +// The various test cases are passed as a list of TestScenario structs. +class CrossSiteDocumentResourceHandlerTest + : public testing::Test, + public testing::WithParamInterface<TestScenario> { + public: + CrossSiteDocumentResourceHandlerTest() + : stream_sink_status_( + net::URLRequestStatus::FromError(net::ERR_IO_PENDING)) { + IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); + } + + // Sets up the request, downstream ResourceHandler, test ResourceHandler, and + // ResourceLoader. + void Initialize(const std::string& target_url, + ResourceType resource_type, + const std::string& initiator_origin, + OriginHeader cors_request) { + stream_sink_status_ = net::URLRequestStatus::FromError(net::ERR_IO_PENDING); + + // Initialize |request_| from the parameters. + request_ = context_.CreateRequest(GURL(target_url), net::DEFAULT_PRIORITY, + &delegate_, TRAFFIC_ANNOTATION_FOR_TESTS); + ResourceRequestInfo::AllocateForTesting(request_.get(), resource_type, + nullptr, // context + 3, // render_process_id + 2, // render_view_id + 1, // render_frame_id + true, // is_main_frame + true, // allow_download + true, // is_async + PREVIEWS_OFF); // previews_state + request_->set_initiator(url::Origin::Create(GURL(initiator_origin))); + + // Create a sink handler to capture results. + auto stream_sink = std::make_unique<TestResourceHandler>( + &stream_sink_status_, &stream_sink_body_); + stream_sink_ = stream_sink->GetWeakPtr(); + + // Create the CrossSiteDocumentResourceHandler. + bool is_nocors_plugin_request = + resource_type == RESOURCE_TYPE_PLUGIN_RESOURCE && + cors_request == OriginHeader::kOmit; + document_blocker_ = std::make_unique<CrossSiteDocumentResourceHandler>( + std::move(stream_sink), request_.get(), is_nocors_plugin_request); + + // Create a mock loader to drive the CrossSiteDocumentResourceHandler. + mock_loader_ = + std::make_unique<MockResourceLoader>(document_blocker_.get()); + } + + // Returns a ResourceResponse that matches the TestScenario's parameters. + scoped_refptr<ResourceResponse> CreateResponse( + const char* response_mime_type, + bool include_no_sniff_header, + AccessControlAllowOriginHeader cors_response, + const char* initiator_origin) { + scoped_refptr<ResourceResponse> response = + base::MakeRefCounted<ResourceResponse>(); + response->head.mime_type = response_mime_type; + scoped_refptr<net::HttpResponseHeaders> response_headers = + base::MakeRefCounted<net::HttpResponseHeaders>(""); + + // No sniff header. + if (include_no_sniff_header) + response_headers->AddHeader("X-Content-Type-Options: nosniff"); + + // CORS header. + if (cors_response == AccessControlAllowOriginHeader::kAllowAny) { + response_headers->AddHeader("Access-Control-Allow-Origin: *"); + } else if (cors_response == + AccessControlAllowOriginHeader::kAllowInitiatorOrigin) { + response_headers->AddHeader(base::StringPrintf( + "Access-Control-Allow-Origin: %s", initiator_origin)); + } else if (cors_response == AccessControlAllowOriginHeader::kAllowNull) { + response_headers->AddHeader("Access-Control-Allow-Origin: null"); + } else if (cors_response == + AccessControlAllowOriginHeader::kAllowExampleDotCom) { + response_headers->AddHeader( + "Access-Control-Allow-Origin: http://example.com"); + } + + response->head.headers = response_headers; + + return response; + } + + protected: + TestBrowserThreadBundle thread_bundle_; + net::TestURLRequestContext context_; + net::TestDelegate delegate_; + std::unique_ptr<net::URLRequest> request_; + + // |stream_sink_| is the handler that's immediately after |document_blocker_| + // in the ResourceHandler chain; it records the values passed to it into + // |stream_sink_status_| and |stream_sink_body_|, which our tests assert + // against. + // + // |stream_sink_| is owned by |document_blocker_|, but we retain a reference + // to it. + base::WeakPtr<TestResourceHandler> stream_sink_; + net::URLRequestStatus stream_sink_status_; + std::string stream_sink_body_; + + // |document_blocker_| is the CrossSiteDocuemntResourceHandler instance under + // test. + std::unique_ptr<CrossSiteDocumentResourceHandler> document_blocker_; + + // |mock_loader_| is the mock loader used to drive |document_blocker_|. + std::unique_ptr<MockResourceLoader> mock_loader_; +}; + +// Runs a particular TestScenario (passed as the test's parameter) through the +// ResourceLoader and CrossSiteDocumentResourceHandler, verifying that the +// response is correctly allowed or blocked based on the scenario. +TEST_P(CrossSiteDocumentResourceHandlerTest, ResponseBlocking) { + const TestScenario scenario = GetParam(); + SCOPED_TRACE(testing::Message() + << "\nScenario at " << __FILE__ << ":" << scenario.source_line); + + Initialize(scenario.target_url, scenario.resource_type, + scenario.initiator_origin, scenario.cors_request); + base::HistogramTester histograms; + + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnWillStart(request_->url())); + + // Set up response based on scenario. + scoped_refptr<ResourceResponse> response = CreateResponse( + scenario.response_mime_type, scenario.include_no_sniff_header, + scenario.cors_response, scenario.initiator_origin); + + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnResponseStarted(response)); + + // Verify MIME type was classified correctly. + EXPECT_EQ(scenario.canonical_mime_type, + document_blocker_->canonical_mime_type_); + + // Verify that we correctly decide whether to block based on headers. Note + // that this includes cases that will later be allowed after sniffing. + bool expected_to_block_based_on_headers = + scenario.verdict == Verdict::kBlockWithoutSniffing || + scenario.verdict == Verdict::kBlockAfterSniffing || + scenario.verdict == Verdict::kAllowAfterSniffing; + EXPECT_EQ(expected_to_block_based_on_headers, + document_blocker_->should_block_based_on_headers_); + + // Verify that we will sniff content into a different buffer if sniffing is + // needed. Note that the different buffer is used even for blocking cases + // where no sniffing is needed, to avoid complexity in the handler. The + // handler doesn't look at the data in that case, but there's no way to verify + // it in the test. + bool expected_to_sniff = scenario.verdict == Verdict::kAllowAfterSniffing || + scenario.verdict == Verdict::kBlockAfterSniffing; + EXPECT_EQ(expected_to_sniff, document_blocker_->needs_sniffing_); + + // Tell the ResourceHandlers to allocate the buffer for reading. In this + // test, the buffer will be allocated immediately by the downstream handler + // and possibly replaced by a different buffer for sniffing. + ASSERT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->OnWillRead()); + EXPECT_EQ(1, stream_sink_->on_will_read_called()); + EXPECT_NE(nullptr, mock_loader_->io_buffer()); + if (expected_to_sniff || scenario.verdict == Verdict::kBlockWithoutSniffing) { + EXPECT_EQ(mock_loader_->io_buffer(), document_blocker_->local_buffer_.get()) + << "Should have used a different IOBuffer for sniffing"; + } else { + EXPECT_EQ(mock_loader_->io_buffer(), stream_sink_->buffer()) + << "Should have used original IOBuffer when sniffing not needed"; + } + + // Deliver the first chunk of the response body; this allows sniffing to + // occur. + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnReadCompleted(scenario.first_chunk)); + EXPECT_EQ(nullptr, mock_loader_->io_buffer()); + + // Verify that the response is blocked or allowed as expected. + bool should_be_blocked = scenario.verdict == Verdict::kBlockWithoutSniffing || + scenario.verdict == Verdict::kBlockAfterSniffing; + if (should_be_blocked) { + EXPECT_EQ("", stream_sink_body_) + << "Response should not have been delivered to the renderer."; + EXPECT_TRUE(document_blocker_->blocked_read_completed_); + EXPECT_FALSE(document_blocker_->allow_based_on_sniffing_); + } else { + EXPECT_EQ(scenario.first_chunk, stream_sink_body_) + << "Response should have been delivered to the renderer."; + EXPECT_FALSE(document_blocker_->blocked_read_completed_); + if (scenario.verdict == Verdict::kAllowAfterSniffing) + EXPECT_TRUE(document_blocker_->allow_based_on_sniffing_); + } + + if (should_be_blocked) { + // The next OnWillRead should cancel and complete the response. + ASSERT_EQ(MockResourceLoader::Status::CANCELED, mock_loader_->OnWillRead()); + net::URLRequestStatus status(net::URLRequestStatus::CANCELED, + net::ERR_ABORTED); + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnResponseCompleted(status)); + } else { + // Simulate the next read being empty to end the response. + ASSERT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->OnWillRead()); + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnReadCompleted("")); + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnResponseCompleted( + net::URLRequestStatus::FromError(net::OK))); + } + + // Verify that histograms are correctly incremented. + base::HistogramTester::CountsMap expected_counts; + std::string histogram_base = "SiteIsolation.XSD.Browser"; + std::string bucket; + switch (scenario.canonical_mime_type) { + case CROSS_SITE_DOCUMENT_MIME_TYPE_HTML: + bucket = "HTML"; + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_XML: + bucket = "XML"; + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_JSON: + bucket = "JSON"; + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN: + bucket = "Plain"; + break; + case CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS: + EXPECT_FALSE(should_be_blocked); + bucket = "Others"; + break; + default: + NOTREACHED(); + } + int start_action = static_cast<int>( + CrossSiteDocumentResourceHandler::Action::kResponseStarted); + int end_action = -1; + switch (scenario.verdict) { + case Verdict::kBlockWithoutSniffing: + end_action = static_cast<int>( + CrossSiteDocumentResourceHandler::Action::kBlockedWithoutSniffing); + break; + case Verdict::kBlockAfterSniffing: + end_action = static_cast<int>( + CrossSiteDocumentResourceHandler::Action::kBlockedAfterSniffing); + break; + case Verdict::kAllowWithoutSniffing: + end_action = static_cast<int>( + CrossSiteDocumentResourceHandler::Action::kAllowedWithoutSniffing); + break; + case Verdict::kAllowAfterSniffing: + end_action = static_cast<int>( + CrossSiteDocumentResourceHandler::Action::kAllowedAfterSniffing); + break; + default: + NOTREACHED(); + } + // Expecting two actions: ResponseStarted and one of the outcomes. + expected_counts[histogram_base + ".Action"] = 2; + EXPECT_THAT(histograms.GetAllSamples(histogram_base + ".Action"), + testing::ElementsAre(base::Bucket(start_action, 1), + base::Bucket(end_action, 1))) + << "Should have incremented the right actions."; + // Expect to hear the number of bytes in the first read when sniffing is + // required. + if (expected_to_sniff) { + std::string first_chunk = scenario.first_chunk; + expected_counts[histogram_base + ".BytesReadForSniffing"] = 1; + EXPECT_EQ( + 1, histograms.GetBucketCount(histogram_base + ".BytesReadForSniffing", + first_chunk.size())); + } + if (should_be_blocked) { + expected_counts[histogram_base + ".Blocked"] = 1; + expected_counts[histogram_base + ".Blocked." + bucket] = 1; + EXPECT_THAT(histograms.GetAllSamples(histogram_base + ".Blocked"), + testing::ElementsAre(base::Bucket(scenario.resource_type, 1))) + << "Should have incremented aggregate blocking."; + EXPECT_THAT(histograms.GetAllSamples(histogram_base + ".Blocked." + bucket), + testing::ElementsAre(base::Bucket(scenario.resource_type, 1))) + << "Should have incremented blocking for resource type."; + } + // Make sure that the expected metrics, and only those metrics, were + // incremented. + EXPECT_THAT(histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser"), + testing::ContainerEq(expected_counts)); +} + +// Similar to the ResponseBlocking test above, but simulates the case that the +// downstream handler does not immediately resume from OnWillRead, in which case +// the downstream buffer may not be allocated until later. +TEST_P(CrossSiteDocumentResourceHandlerTest, OnWillReadDefer) { + const TestScenario scenario = GetParam(); + SCOPED_TRACE(testing::Message() + << "\nScenario at " << __FILE__ << ":" << scenario.source_line); + + Initialize(scenario.target_url, scenario.resource_type, + scenario.initiator_origin, scenario.cors_request); + + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnWillStart(request_->url())); + + // Set up response based on scenario. + scoped_refptr<ResourceResponse> response = CreateResponse( + scenario.response_mime_type, scenario.include_no_sniff_header, + scenario.cors_response, scenario.initiator_origin); + + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnResponseStarted(response)); + + // Verify that we will sniff content into a different buffer if sniffing is + // needed. Note that the different buffer is used even for blocking cases + // where no sniffing is needed, to avoid complexity in the handler. The + // handler doesn't look at the data in that case, but there's no way to verify + // it in the test. + bool expected_to_sniff = scenario.verdict == Verdict::kAllowAfterSniffing || + scenario.verdict == Verdict::kBlockAfterSniffing; + EXPECT_EQ(expected_to_sniff, document_blocker_->needs_sniffing_); + + // Cause the TestResourceHandler to defer when OnWillRead is called, to make + // sure the test scenarios still work when the downstream handler's buffer + // isn't allocated in the same call. + stream_sink_->set_defer_on_will_read(true); + ASSERT_EQ(MockResourceLoader::Status::CALLBACK_PENDING, + mock_loader_->OnWillRead()); + EXPECT_EQ(1, stream_sink_->on_will_read_called()); + + // No buffers have been allocated yet. + EXPECT_EQ(nullptr, mock_loader_->io_buffer()); + EXPECT_EQ(nullptr, document_blocker_->local_buffer_.get()); + + // Resume the downstream handler, which should establish a buffer for the + // ResourceLoader (either the downstream one or a local one for sniffing). + stream_sink_->Resume(); + EXPECT_NE(nullptr, mock_loader_->io_buffer()); + if (expected_to_sniff || scenario.verdict == Verdict::kBlockWithoutSniffing) { + EXPECT_EQ(mock_loader_->io_buffer(), document_blocker_->local_buffer_.get()) + << "Should have used a different IOBuffer for sniffing"; + } else { + EXPECT_EQ(mock_loader_->io_buffer(), stream_sink_->buffer()) + << "Should have used original IOBuffer when sniffing not needed"; + } + + // Deliver the first chunk of the response body; this allows sniffing to + // occur. + ASSERT_EQ(MockResourceLoader::Status::IDLE, + mock_loader_->OnReadCompleted(scenario.first_chunk)); + EXPECT_EQ(nullptr, mock_loader_->io_buffer()); + + // Verify that the response is blocked or allowed as expected. + if (scenario.verdict == Verdict::kBlockWithoutSniffing || + scenario.verdict == Verdict::kBlockAfterSniffing) { + EXPECT_EQ("", stream_sink_body_) + << "Response should not have been delivered to the renderer."; + EXPECT_TRUE(document_blocker_->blocked_read_completed_); + EXPECT_FALSE(document_blocker_->allow_based_on_sniffing_); + } else { + EXPECT_EQ(scenario.first_chunk, stream_sink_body_) + << "Response should have been delivered to the renderer."; + EXPECT_FALSE(document_blocker_->blocked_read_completed_); + if (scenario.verdict == Verdict::kAllowAfterSniffing) + EXPECT_TRUE(document_blocker_->allow_based_on_sniffing_); + } +} + +INSTANTIATE_TEST_CASE_P(, + CrossSiteDocumentResourceHandlerTest, + ::testing::ValuesIn(kScenarios)); + +} // namespace content diff --git a/chromium/content/browser/loader/resource_dispatcher_host_impl.cc b/chromium/content/browser/loader/resource_dispatcher_host_impl.cc index 88663ce2ae2..b372f8a6f76 100644 --- a/chromium/content/browser/loader/resource_dispatcher_host_impl.cc +++ b/chromium/content/browser/loader/resource_dispatcher_host_impl.cc @@ -44,6 +44,7 @@ #include "content/browser/child_process_security_policy_impl.h" #include "content/browser/frame_host/navigation_request_info.h" #include "content/browser/loader/async_resource_handler.h" +#include "content/browser/loader/cross_site_document_resource_handler.h" #include "content/browser/loader/detachable_resource_handler.h" #include "content/browser/loader/intercepting_resource_handler.h" #include "content/browser/loader/loader_delegate.h" @@ -1489,7 +1490,7 @@ ResourceDispatcherHostImpl::CreateResourceHandler( } return AddStandardHandlers(request, request_data.resource_type, - resource_context, + resource_context, request_data.fetch_request_mode, request_data.fetch_request_context_type, request_data.fetch_mixed_content_context_type, requester_info->appcache_service(), child_id, @@ -1518,6 +1519,7 @@ ResourceDispatcherHostImpl::AddStandardHandlers( net::URLRequest* request, ResourceType resource_type, ResourceContext* resource_context, + FetchRequestMode fetch_request_mode, RequestContextType fetch_request_context_type, blink::WebMixedContentContextType fetch_mixed_content_context_type, AppCacheService* appcache_service, @@ -1616,6 +1618,17 @@ ResourceDispatcherHostImpl::AddStandardHandlers( handler.reset(new ThrottlingResourceHandler( std::move(handler), request, std::move(pre_mime_sniffing_throttles))); + if (!IsResourceTypeFrame(resource_type)) { + // Add a handler to block cross-site documents from the renderer process. + // This should be pre mime-sniffing, since it affects whether the response + // will be read, and since it looks at the original mime type. + bool is_nocors_plugin_request = + resource_type == RESOURCE_TYPE_PLUGIN_RESOURCE && + fetch_request_mode == FETCH_REQUEST_MODE_NO_CORS; + handler.reset(new CrossSiteDocumentResourceHandler( + std::move(handler), request, is_nocors_plugin_request)); + } + return handler; } @@ -2188,11 +2201,12 @@ void ResourceDispatcherHostImpl::BeginNavigationRequest( ->stream() ->CreateHandle(); + // Safe to consider navigations as NO_CORS. // TODO(davidben): Fix the dependency on child_id/route_id. Those are used // by the ResourceScheduler. currently it's a no-op. handler = AddStandardHandlers( new_request.get(), resource_type, resource_context, - info.begin_params.request_context_type, + FETCH_REQUEST_MODE_NO_CORS, info.begin_params.request_context_type, info.begin_params.mixed_content_context_type, appcache_handle_core ? appcache_handle_core->GetAppCacheService() : nullptr, diff --git a/chromium/content/browser/loader/resource_dispatcher_host_impl.h b/chromium/content/browser/loader/resource_dispatcher_host_impl.h index 3ef287d8739..2fb715de446 100644 --- a/chromium/content/browser/loader/resource_dispatcher_host_impl.h +++ b/chromium/content/browser/loader/resource_dispatcher_host_impl.h @@ -638,6 +638,7 @@ class CONTENT_EXPORT ResourceDispatcherHostImpl net::URLRequest* request, ResourceType resource_type, ResourceContext* resource_context, + FetchRequestMode fetch_request_mode, RequestContextType fetch_request_context_type, blink::WebMixedContentContextType fetch_mixed_content_context_type, AppCacheService* appcache_service, diff --git a/chromium/content/browser/loader/url_loader_factory_impl_unittest.cc b/chromium/content/browser/loader/url_loader_factory_impl_unittest.cc index 7d58b15a607..fcdfcb865df 100644 --- a/chromium/content/browser/loader/url_loader_factory_impl_unittest.cc +++ b/chromium/content/browser/loader/url_loader_factory_impl_unittest.cc @@ -162,8 +162,8 @@ TEST_P(URLLoaderFactoryImplTest, GetResponse) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), kRoutingId, kRequestId, mojom::kURLLoadOptionNone, request, client.CreateInterfacePtr(), @@ -239,8 +239,8 @@ TEST_P(URLLoaderFactoryImplTest, GetFailedResponse) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), 2, 1, mojom::kURLLoadOptionNone, request, client.CreateInterfacePtr(), @@ -269,8 +269,8 @@ TEST_P(URLLoaderFactoryImplTest, GetFailedResponse2) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), 2, 1, mojom::kURLLoadOptionNone, request, client.CreateInterfacePtr(), @@ -296,8 +296,8 @@ TEST_P(URLLoaderFactoryImplTest, InvalidURL) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); ASSERT_FALSE(request.url.is_valid()); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), 2, 1, mojom::kURLLoadOptionNone, request, @@ -324,8 +324,8 @@ TEST_P(URLLoaderFactoryImplTest, ShouldNotRequestURL) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), 2, 1, mojom::kURLLoadOptionNone, request, client.CreateInterfacePtr(), @@ -355,7 +355,7 @@ TEST_P(URLLoaderFactoryImplTest, DownloadToFile) { request.method = "GET"; request.resource_type = RESOURCE_TYPE_XHR; request.download_to_file = true; - request.request_initiator = url::Origin(); + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), kRoutingId, kRequestId, 0, request, client.CreateInterfacePtr(), @@ -423,7 +423,7 @@ TEST_P(URLLoaderFactoryImplTest, DownloadToFileFailure) { request.method = "GET"; request.resource_type = RESOURCE_TYPE_XHR; request.download_to_file = true; - request.request_initiator = url::Origin(); + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), kRoutingId, kRequestId, 0, request, client.CreateInterfacePtr(), @@ -484,8 +484,8 @@ TEST_P(URLLoaderFactoryImplTest, OnTransferSizeUpdated) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); request.report_raw_headers = true; factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), kRoutingId, kRequestId, @@ -546,8 +546,8 @@ TEST_P(URLLoaderFactoryImplTest, CancelFromRenderer) { // enabled, the url scheme of frame type requests from the renderer process // must be blob scheme. request.resource_type = RESOURCE_TYPE_XHR; - // Need to set |request_initiator| for non main frame type request. - request.request_initiator = url::Origin(); + // Need to set same-site |request_initiator| for non main frame type request. + request.request_initiator = url::Origin::Create(request.url); factory_->CreateLoaderAndStart( mojo::MakeRequest(&loader), kRoutingId, kRequestId, mojom::kURLLoadOptionNone, request, client.CreateInterfacePtr(), |