summaryrefslogtreecommitdiff
path: root/chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc')
-rw-r--r--chromium/content/browser/loader/cross_site_document_resource_handler_unittest.cc836
1 files changed, 836 insertions, 0 deletions
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