summaryrefslogtreecommitdiff
path: root/chromium/content/browser/loader
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/content/browser/loader')
-rw-r--r--chromium/content/browser/loader/DEPS1
-rw-r--r--chromium/content/browser/loader/cross_site_document_blocking_browsertest.cc380
-rw-r--r--chromium/content/browser/loader/cross_site_document_resource_handler.cc420
-rw-r--r--chromium/content/browser/loader/cross_site_document_resource_handler.h156
-rw-r--r--chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc836
-rw-r--r--chromium/content/browser/loader/resource_dispatcher_host_impl.cc18
-rw-r--r--chromium/content/browser/loader/resource_dispatcher_host_impl.h1
-rw-r--r--chromium/content/browser/loader/url_loader_factory_impl_unittest.cc32
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(),