| // 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 |