| // 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 <algorithm> |
| #include <initializer_list> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #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/weak_ptr.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "content/browser/loader/intercepting_resource_handler.h" |
| #include "content/browser/loader/mime_sniffing_resource_handler.h" |
| #include "content/browser/loader/mock_resource_loader.h" |
| #include "content/browser/loader/resource_controller.h" |
| #include "content/browser/loader/resource_dispatcher_host_impl.h" |
| #include "content/browser/loader/test_resource_handler.h" |
| #include "content/public/browser/resource_request_info.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 "content/test/fake_plugin_service.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 "services/network/public/cpp/resource_response.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| using MimeType = network::CrossOriginReadBlocking::MimeType; |
| |
| namespace content { |
| |
| namespace { |
| |
| enum class OriginHeader { kOmit, kInclude }; |
| |
| enum class AccessControlAllowOriginHeader { |
| kOmit, |
| kAllowAny, |
| kAllowNull, |
| kAllowInitiatorOrigin, |
| kAllowExampleDotCom |
| }; |
| |
| enum class Verdict { |
| kAllow, |
| kBlock, |
| }; |
| |
| constexpr int kVerdictPacketForHeadersBasedVerdict = -1; |
| |
| // 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_content_type; |
| MimeType canonical_mime_type; |
| bool include_no_sniff_header; |
| bool simulate_range_response; |
| AccessControlAllowOriginHeader cors_response; |
| // |packets| specifies the response data which may arrive over the course of |
| // several writes. |
| std::initializer_list<const char*> packets; |
| |
| std::string data() const { |
| std::string data; |
| for (const char* packet : packets) { |
| data += packet; |
| } |
| return data; |
| } |
| |
| // Expected result. |
| Verdict verdict; |
| // The packet number during which the verdict is decided. |
| // kVerdictPacketForHeadersBasedVerdict means that the verdict can be decided |
| // before the first packet's data is available. |packets.size()| means that |
| // the verdict is decided during the end-of-stream call. |
| int verdict_packet; |
| }; |
| |
| // 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; |
| } |
| |
| std::string verdict; |
| switch (scenario.verdict) { |
| case Verdict::kAllow: |
| verdict = "Verdict::kAllow"; |
| break; |
| case Verdict::kBlock: |
| verdict = "Verdict::kBlock"; |
| break; |
| } |
| |
| std::string packets = "{"; |
| for (std::string packet : scenario.packets) { |
| base::ReplaceChars(packet, "\\", "\\\\", &packet); |
| base::ReplaceChars(packet, "\"", "\\\"", &packet); |
| base::ReplaceChars(packet, "\n", "\\n", &packet); |
| base::ReplaceChars(packet, "\t", "\\t", &packet); |
| base::ReplaceChars(packet, "\r", "\\r", &packet); |
| if (packets.length() > 1) |
| packets += ", "; |
| packets += "\""; |
| packets += packet; |
| packets += "\""; |
| } |
| packets += "}"; |
| |
| 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_content_type = " << scenario.response_content_type |
| << "\n canonical_mime_type = " << scenario.canonical_mime_type |
| << "\n include_no_sniff = " |
| << (scenario.include_no_sniff_header ? "true" : "false") |
| << "\n range_response = " |
| << (scenario.simulate_range_response ? "true" : "false") |
| << "\n cors_response = " << cors_response |
| << "\n packets = " << packets |
| << "\n verdict = " << verdict |
| << "\n verdict_packet = " << scenario.verdict_packet; |
| } |
| |
| // An HTML response with an HTML comment that's longer than the sniffing |
| // threshhold. We don't sniff past net::kMaxBytesToSniff, so these are not |
| // protected |
| const char kHTMLWithTooLongComment[] = |
| "<!--.............................................................72 chars" |
| "................................................................144 chars" |
| "................................................................216 chars" |
| "................................................................288 chars" |
| "................................................................360 chars" |
| "................................................................432 chars" |
| "................................................................504 chars" |
| "................................................................576 chars" |
| "................................................................648 chars" |
| "................................................................720 chars" |
| "................................................................792 chars" |
| "................................................................864 chars" |
| "................................................................936 chars" |
| "...............................................................1008 chars" |
| "...............................................................1080 chars" |
| "--><html><head>"; |
| |
| // 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 (without sniffing): |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Allowed: Same-origin JSON with parser breaker and HTML mime type", |
| __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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {")]}',\n[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Allowed: Same-origin JSON with parser breaker and JSON mime type", |
| __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/json", // response_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {")]}'\n[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Allowed: Cross-site script without parser breaker", |
| __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_content_type |
| MimeType::kOthers, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"var x=3;"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowInitiatorOrigin, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowAny, // cors_response |
| {"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowNull, // cors_response |
| {"{\"x\" : 3}"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| // Blocked, because the unit test doesn't make a call to |
| // CrossOriginReadBlocking::AddExceptionForFlash (simulating a behavior |
| // of a compromised renderer that only pretends to be hosting Flash). |
| // |
| // The regular scenario is covered by integration tests: |
| // OutOfProcessPPAPITest.URLLoaderTrusted. |
| "Blocked: 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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowInitiatorOrigin, // cors_response |
| {"<html><head>this should sniff as HTML"}, // first_chunk |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Allowed: JSON object + CORS with parser-breaker labeled as JavaScript", |
| __LINE__, |
| "http://www.b.com/resource.html", // target_url |
| RESOURCE_TYPE_SCRIPT, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kInclude, // cors_request |
| "application/javascript", // response_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowAny, // cors_response |
| {")]}'\n[true, false]"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Blocked: JSON object labeled as JavaScript with a no-sniff header", |
| __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_content_type |
| MimeType::kOthers, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"{ \"key\"", ": true }"}, // packets |
| Verdict::kBlock, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "Allowed: Empty response with PNG mime type", |
| __LINE__, |
| "http://www.b.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "image/png", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: Empty response with PNG mime type and nosniff header", |
| __LINE__, |
| "http://www.b.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "image/png", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| |
| // 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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"foo({\"x\" : 3})"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"var x = 3;"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: JSON-like 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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"{", " \n", "var x = 3;\n", "console.log('hello');"}, // packets |
| Verdict::kAllow, // verdict |
| 2, // verdict_packet |
| }, |
| |
| { |
| "Allowed: JSONP labeled as JSON", |
| __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/json", // response_content_type |
| MimeType::kJson, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"invoke({ \"key\": true });"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed (for now): JSON array literal labeled as text/plain", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"[1, 2, {}, true, false, \"yay\"]"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: JSON array literal on which a function is called.", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"[1, 2, {}, true, false, \"yay\"]", ".map(x => console.log(x))", |
| ".map(x => console.log(x));"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kXml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"Won't sniff as XML"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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/json", // response_content_type |
| MimeType::kJson, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"Won't sniff as JSON"}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: Cross-site XHR to partial match for <HTML> tag", |
| __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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<htm"}, // packets |
| Verdict::kAllow, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "Allowed: HTML tag appears only after net::kMaxBytesToSniff", |
| __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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {kHTMLWithTooLongComment}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: Empty response with html mime type", |
| __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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {}, // packets |
| Verdict::kAllow, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: Same-site XHR to a filesystem URI", |
| __LINE__, |
| "filesystem:http://www.a.com/file.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Allowed: Same-site XHR to a blob URI", |
| __LINE__, |
| "blob:http://www.a.com/guid-goes-here", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kAllow, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| |
| // Blocked responses (without sniffing): |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Blocked: nosniff + Content-Type: text/html; charset=utf-8", |
| __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; charset=utf-8", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"Wouldn't sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-origin, same-site XHR to nosniff HTML without CORS", |
| __LINE__, |
| "https://foo.site.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "https://bar.site.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-origin XHR to HTML with wrong CORS (okay same-site)", |
| // Note that initiator_origin is cross-origin, but same-site in relation |
| // to the CORS response. |
| __LINE__, |
| "http://www.b.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://foo.example.com/", // initiator_origin |
| OriginHeader::kInclude, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowExampleDotCom, // cors_response |
| {"<hTmL><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| |
| { |
| // This scenario is unusual, since there's no difference between |
| // a blocked response and a non-blocked response; the CSDRH doesn't |
| // actually have a chance to cancel the connection. |
| "Blocked(-ish?): Nosniff header + empty response", |
| __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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| |
| // Blocked responses due to sniffing: |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kXml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kJson, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"{\"x\" : 3}"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Blocked: slow-arriving JSON labeled as text/plain", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {" ", "\t", "{", "\"x\" ", " ", ": 3}"}, // packets |
| Verdict::kBlock, // verdict |
| 5, // verdict_packet |
| }, |
| { |
| "Blocked: slow-arriving xml labeled as text/plain", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {" ", "\t", "<", "?", "x", "m", "l", ">"}, // packets |
| Verdict::kBlock, // verdict |
| 6, // verdict_packet |
| }, |
| { |
| "Blocked: slow-arriving html labeled as text/plain", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {" <!--", "\t -", "-", "->", "\n", "<", "s", "c", "r", "i", "p", |
| "t"}, // packets |
| Verdict::kBlock, // verdict |
| 11, // verdict_packet |
| }, |
| { |
| "Blocked: slow-arriving html with commented-out xml tag", |
| __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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {" <!--", " <?xml ", "-->\n", "<", "h", "e", "a", "d"}, // packets |
| Verdict::kBlock, // verdict |
| 7, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<!doc", "type html><html itemscope=\"\" ", |
| "itemtype=\"http://schema.org/SearchResultsPage\" ", |
| "lang=\"en\"><head>"}, // packets |
| Verdict::kBlock, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowExampleDotCom, // cors_response |
| {"<hTmL><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "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_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // first_chunk |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site JSON with parser breaker and JSON mime type", |
| __LINE__, |
| "http://a.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://c.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/json", // response_content_type |
| MimeType::kJson, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {")]", "}'\n[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kBlock, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site JSON with parser breaker/nosniff/other mime type", |
| __LINE__, |
| "http://a.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://c.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "audio/x-wav", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {")]", "}'\n[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kBlock, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site JSON with parser breaker and other mime type", |
| __LINE__, |
| "http://a.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://c.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "application/javascript", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"for(;;)", ";[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kBlock, // verdict |
| 1, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site JSON with parser breaker/html/nosniff", |
| __LINE__, |
| "http://a.com/resource.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://c.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {")]", "}'\n[true, true, false, \"user@chromium.org\"]"}, // packets |
| Verdict::kBlock, // verdict |
| kVerdictPacketForHeadersBasedVerdict, // verdict_packet |
| }, |
| { |
| "Blocked: JSON object + mismatching CORS with parser-breaker labeled " |
| "as JavaScript", |
| __LINE__, |
| "http://www.b.com/resource.html", // target_url |
| RESOURCE_TYPE_SCRIPT, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kInclude, // cors_request |
| "application/javascript", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| true, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowExampleDotCom, // cors_response |
| {")]}'\n[true, false]"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site XHR to a filesystem URI", |
| __LINE__, |
| "filesystem:http://www.b.com/file.html", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Blocked: Cross-site XHR to a blob URI", |
| __LINE__, |
| "blob:http://www.b.com/guid-goes-here", // target_url |
| RESOURCE_TYPE_XHR, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| false, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"<html><head>this should sniff as HTML"}, // packets |
| Verdict::kBlock, // verdict |
| 0, // verdict_packet |
| }, |
| { |
| "Allowed: Javascript 206", |
| __LINE__, |
| "http://www.b.com/script.js", // target_url |
| RESOURCE_TYPE_SCRIPT, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "application/javascript", // response_content_type |
| MimeType::kOthers, // canonical_mime_type |
| false, // include_no_sniff_header |
| true, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"x = 1;"}, // packets |
| Verdict::kAllow, // verdict |
| -1, // verdict_packet |
| }, |
| { |
| "Allowed: text/html 206 media with CORS", |
| __LINE__, |
| "http://www.b.com/movie.html", // target_url |
| RESOURCE_TYPE_MEDIA, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kInclude, // cors_request |
| "text/html", // response_content_type |
| MimeType::kInvalidMimeType, // canonical_mime_type |
| false, // include_no_sniff_header |
| true, // simulate_range_response |
| AccessControlAllowOriginHeader::kAllowInitiatorOrigin, // cors_response |
| {"simulated *middle*-of-html content"}, // packets |
| Verdict::kAllow, // verdict |
| -1, // verdict_packet |
| }, |
| { |
| "Allowed: text/plain 206 media", |
| __LINE__, |
| "http://www.b.com/movie.txt", // target_url |
| RESOURCE_TYPE_MEDIA, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/plain", // response_content_type |
| MimeType::kPlain, // canonical_mime_type |
| false, // include_no_sniff_header |
| true, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"movie content"}, // packets |
| Verdict::kAllow, // verdict |
| -1, // verdict_packet |
| }, |
| { |
| "Blocked: text/html 206 media", |
| __LINE__, |
| "http://www.b.com/movie.html", // target_url |
| RESOURCE_TYPE_MEDIA, // resource_type |
| "http://www.a.com/", // initiator_origin |
| OriginHeader::kOmit, // cors_request |
| "text/html", // response_content_type |
| MimeType::kHtml, // canonical_mime_type |
| false, // include_no_sniff_header |
| true, // simulate_range_response |
| AccessControlAllowOriginHeader::kOmit, // cors_response |
| {"simulated *middle*-of-html content"}, // packets |
| Verdict::kBlock, // verdict |
| -1, // verdict_packet |
| }, |
| }; |
| |
| // TestResourceDispatcherHost is a ResourceDispatcherHostImpl that the test |
| // passes to MimeSniffingResourceHandler. Since MimeSniffingResourceHandler |
| // only calls 2 methods of ResourceDispatcherHostImpl, only these 2 methods have |
| // been overriden below (we expect no calls - this is why the bodies only have |
| // the NOTREACHED macro in them). This pattern of overriding only 2 methods has |
| // been copied from mime_sniffing_resource_handler_unittest.cc |
| class TestResourceDispatcherHost : public ResourceDispatcherHostImpl { |
| public: |
| TestResourceDispatcherHost() {} |
| |
| std::unique_ptr<ResourceHandler> CreateResourceHandlerForDownload( |
| net::URLRequest* request, |
| bool is_content_initiated, |
| bool must_download, |
| bool is_new_request) override { |
| NOTREACHED(); |
| return std::make_unique<TestResourceHandler>(); |
| } |
| |
| std::unique_ptr<ResourceHandler> MaybeInterceptAsStream( |
| net::URLRequest* request, |
| network::ResourceResponse* response, |
| std::string* payload) override { |
| NOTREACHED(); |
| return std::make_unique<TestResourceHandler>(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(TestResourceDispatcherHost); |
| }; |
| |
| } // 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, and the chain of 1) MockResourceLoader, 2) |
| // CrossSiteDocumentResourceHandler, 3) TestResourceHandler. If |
| // |inject_mime_sniffer| is specified then a MimeSniffingResourceHandler will |
| // be injected between MockResourceLoader and |
| // CrossSiteDocumentResourceHandler. |
| void Initialize(const std::string& target_url, |
| ResourceType resource_type, |
| const std::string& initiator_origin, |
| OriginHeader cors_request, |
| bool inject_mime_sniffer) { |
| 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 |
| ResourceInterceptPolicy::kAllowNone, // resource_intercept_policy |
| true, // is_async |
| PREVIEWS_OFF, // previews_state |
| nullptr); // navigation_ui_data |
| 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. |
| auto fetch_request_mode = cors_request == OriginHeader::kOmit |
| ? network::mojom::FetchRequestMode::kNoCors |
| : network::mojom::FetchRequestMode::kCors; |
| auto document_blocker = std::make_unique<CrossSiteDocumentResourceHandler>( |
| std::move(stream_sink), request_.get(), fetch_request_mode); |
| document_blocker_ = document_blocker.get(); |
| first_handler_ = std::move(document_blocker); |
| |
| // Inject MimeSniffingResourceHandler if requested. |
| if (inject_mime_sniffer) { |
| intercepting_handler_ = std::make_unique<InterceptingResourceHandler>( |
| std::make_unique<TestResourceHandler>(), nullptr); |
| first_handler_ = std::make_unique<MimeSniffingResourceHandler>( |
| std::move(first_handler_), &dispatcher_host_, &plugin_service_, |
| intercepting_handler_.get(), request_.get(), |
| blink::mojom::RequestContextType::SCRIPT); |
| } |
| |
| // Create a mock loader to drive our chain of resource loaders. |
| mock_loader_ = std::make_unique<MockResourceLoader>(first_handler_.get()); |
| } |
| |
| // Returns a ResourceResponse that matches the TestScenario's parameters. |
| scoped_refptr<network::ResourceResponse> CreateResponse( |
| const char* response_content_type, |
| bool include_no_sniff_header, |
| bool simulate_range_response, |
| AccessControlAllowOriginHeader cors_response, |
| const char* initiator_origin) { |
| scoped_refptr<network::ResourceResponse> response = |
| base::MakeRefCounted<network::ResourceResponse>(); |
| scoped_refptr<net::HttpResponseHeaders> response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>( |
| simulate_range_response ? "HTTP/1.1 206 OK" : "HTTP/1.1 200 OK"); |
| |
| // Content-Type header. |
| std::string charset; |
| bool had_charset = false; |
| response_headers->AddHeader(std::string("Content-Type: ") + |
| response_content_type); |
| net::HttpUtil::ParseContentType(response_content_type, |
| &response->head.mime_type, &charset, |
| &had_charset, nullptr); |
| EXPECT_FALSE(response->head.mime_type.empty()) |
| << "Invalid MIME type defined in kScenarios."; |
| |
| // No sniff header. |
| if (include_no_sniff_header) |
| response_headers->AddHeader("X-Content-Type-Options: nosniff"); |
| |
| // Range response. The product code doesn't currently look at the exact |
| // range specified, so we can get away with testing with arbitrary/random |
| // values. |
| if (simulate_range_response) |
| response_headers->AddHeader("Content-Range: bytes 200-1000/67589"); |
| |
| // 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; |
| } |
| |
| // Determines the number of times that OnWillRead should be called on |
| // |stream_sink_| at the point in the test indicated by |current_packet|. |
| int GetExpectedNumberOfOnWillReadCalls(int current_packet) { |
| TestScenario scenario = GetParam(); |
| |
| // In all block scenarios, OnWillRead is called only once. |
| if (scenario.verdict == Verdict::kBlock) |
| return 1; |
| |
| // If we're not yet at the point of the verdict packet, OnWillRead has only |
| // been called only once, since the resource handler doesn't yet know if |
| // we're blocking or allowing yet. |
| if (current_packet < scenario.verdict_packet) |
| return 1; |
| |
| // If we are streaming from the start, we'll call |stream_sink_|'s |
| // OnWillRead every time. |
| if (scenario.verdict_packet <= 0) |
| return current_packet + 1; |
| |
| // When the verdict is decided on the last packet (and the response was not |
| // empty), OnWillRead should be called exactly twice. |
| if (scenario.verdict_packet == static_cast<int>(scenario.packets.size())) |
| return 2; |
| |
| // Otherwise, we buffer up until the verdict_packet, and then stream |
| // thereafter. |
| return 1 + (current_packet - scenario.verdict_packet); |
| } |
| |
| 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. |
| CrossSiteDocumentResourceHandler* document_blocker_ = nullptr; |
| |
| // |first_handler_| is the first resource handler in a chain or resource |
| // handlers that eventually reached CrossSiteDocumentResourceHandler. |
| std::unique_ptr<LayeredResourceHandler> first_handler_; |
| |
| // |mock_loader_| is the mock loader used to drive |document_blocker_|. |
| std::unique_ptr<MockResourceLoader> mock_loader_; |
| |
| FakePluginService plugin_service_; |
| TestResourceDispatcherHost dispatcher_host_; |
| std::unique_ptr<InterceptingResourceHandler> intercepting_handler_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CrossSiteDocumentResourceHandlerTest); |
| }; |
| |
| // 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, |
| false /* = inject_mime_sniffer */); |
| base::HistogramTester histograms; |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnWillStart(request_->url())); |
| |
| // Set up response based on scenario. |
| scoped_refptr<network::ResourceResponse> response = CreateResponse( |
| scenario.response_content_type, scenario.include_no_sniff_header, |
| scenario.simulate_range_response, scenario.cors_response, |
| scenario.initiator_origin); |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnResponseStarted(response)); |
| |
| // Verify MIME type was classified correctly. |
| ASSERT_TRUE(document_blocker_->has_response_started_); |
| EXPECT_EQ(scenario.canonical_mime_type, |
| document_blocker_->analyzer_->canonical_mime_type()); |
| |
| // 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_packet >= 0; |
| ASSERT_EQ(expected_to_sniff, document_blocker_->analyzer_->needs_sniffing()); |
| |
| // 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 = |
| expected_to_sniff || scenario.verdict == Verdict::kBlock; |
| ASSERT_EQ(expected_to_block_based_on_headers, |
| document_blocker_->should_block_based_on_headers_); |
| |
| // This vector holds the packets to be delivered. |
| std::vector<const char*> packets_vector(scenario.packets); |
| packets_vector.push_back(""); // End-of-stream is marked by an empty packet. |
| |
| bool should_be_blocked = scenario.verdict == Verdict::kBlock; |
| int eof_packet = static_cast<int>(scenario.packets.size()); |
| int effective_verdict_packet = scenario.verdict_packet; |
| if (should_be_blocked) { |
| // Our implementation currently only blocks at the second OnWillRead. |
| effective_verdict_packet = std::max(0, effective_verdict_packet); |
| } |
| |
| for (int packet_index = 0; |
| packet_index < static_cast<int>(packets_vector.size()); packet_index++) { |
| const base::StringPiece packet = packets_vector[packet_index]; |
| SCOPED_TRACE(testing::Message() |
| << "While delivering packet #" << packet_index); |
| bool should_be_streaming = scenario.verdict == Verdict::kAllow && |
| packet_index > scenario.verdict_packet; |
| |
| if (packet_index <= effective_verdict_packet) { |
| EXPECT_FALSE(document_blocker_->blocked_read_completed_); |
| EXPECT_FALSE(document_blocker_->allow_based_on_sniffing_); |
| } else { |
| ASSERT_EQ(should_be_blocked, document_blocker_->blocked_read_completed_); |
| EXPECT_EQ(!should_be_blocked && expected_to_sniff, |
| document_blocker_->allow_based_on_sniffing_); |
| } |
| |
| // Tell the ResourceHandlers to allocate the buffer for reading. On the |
| // first read, and after an 'allow' verdict, this will request a buffer from |
| // the downstream handler. Otherwise, it'll ask for more space in the local |
| // buffer used for sniffing. |
| mock_loader_->OnWillRead(); |
| |
| if (should_be_blocked && packet_index == effective_verdict_packet + 1) { |
| // The Cancel() occurs during the OnWillRead subsequent to the block |
| // decision. |
| EXPECT_EQ(MockResourceLoader::Status::CANCELED, mock_loader_->status()); |
| break; |
| } |
| |
| EXPECT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->status()); |
| EXPECT_NE(nullptr, mock_loader_->io_buffer()); |
| if (!should_be_streaming) { |
| EXPECT_EQ(mock_loader_->io_buffer()->data(), |
| document_blocker_->local_buffer_->data() + |
| document_blocker_->local_buffer_bytes_read_) |
| << "Should have used a different IOBuffer for sniffing"; |
| EXPECT_NE(stream_sink_->buffer(), mock_loader_->io_buffer()); |
| } else { |
| EXPECT_FALSE(should_be_blocked); |
| EXPECT_EQ(mock_loader_->io_buffer(), stream_sink_->buffer()) |
| << "Should have used original IOBuffer when sniffing not needed"; |
| } |
| |
| // Deliver the next packet of the response body; this allows sniffing to |
| // occur. |
| mock_loader_->OnReadCompleted(packet); |
| if (mock_loader_->status() == |
| MockResourceLoader::Status::CALLBACK_PENDING) { |
| // CALLBACK_PENDING is only expected in the case when streaming starts. |
| if (scenario.verdict_packet == kVerdictPacketForHeadersBasedVerdict) { |
| // If not sniffing, then |
| // - if response is allowed, then streaming should start in |
| // OnResponseStarted (and we shouldn't hit CALLBACK_PENDING state). |
| // - if response is blocked, then CALLBACK_PENDING state will happen |
| // when enforcing blocking - at packet #0. |
| EXPECT_EQ(Verdict::kBlock, scenario.verdict); |
| EXPECT_EQ(0, packet_index); |
| } else { |
| // If sniffing, then streaming should start at the verdict packet. |
| EXPECT_EQ(scenario.verdict_packet, packet_index); |
| } |
| |
| // Waits for CrossSiteDocumentResourceHandler::Resume() if needed. |
| mock_loader_->WaitUntilIdleOrCanceled(); |
| } |
| EXPECT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->status()); |
| EXPECT_EQ(nullptr, mock_loader_->io_buffer()); |
| |
| // mock_loader->OnWillRead() should only result in calls through to |
| // stream_sink_->OnWillRead() for the first packet, or for packets after an |
| // "allow" decision. |
| EXPECT_EQ(GetExpectedNumberOfOnWillReadCalls(packet_index), |
| stream_sink_->on_will_read_called()); |
| |
| if (should_be_blocked) { |
| ASSERT_FALSE(document_blocker_->allow_based_on_sniffing_); |
| ASSERT_EQ("", stream_sink_body_) |
| << "Response should not have been delivered to the renderer."; |
| ASSERT_LE(packet_index, effective_verdict_packet); |
| ASSERT_EQ(packet_index == effective_verdict_packet, |
| document_blocker_->blocked_read_completed_); |
| } else if (expected_to_sniff && packet_index >= scenario.verdict_packet) { |
| ASSERT_TRUE(document_blocker_->allow_based_on_sniffing_); |
| ASSERT_FALSE(document_blocker_->blocked_read_completed_); |
| } else { |
| ASSERT_FALSE(document_blocker_->allow_based_on_sniffing_); |
| ASSERT_FALSE(document_blocker_->blocked_read_completed_); |
| } |
| if (should_be_streaming) { |
| ASSERT_LE(packet.size(), stream_sink_body_.size()); |
| EXPECT_EQ(packet, stream_sink_body_.substr(stream_sink_body_.size() - |
| packet.size())) |
| << "Response should be streamed to the renderer."; |
| } |
| } |
| |
| // All packets are now sent. Validate our final expectations, and send |
| // OnResponseCompleted. |
| EXPECT_EQ(GetExpectedNumberOfOnWillReadCalls(eof_packet), |
| stream_sink_->on_will_read_called()); |
| |
| // Check the final block/no-block decision. |
| EXPECT_EQ(document_blocker_->blocked_read_completed_, should_be_blocked); |
| EXPECT_EQ(document_blocker_->allow_based_on_sniffing_, |
| !should_be_blocked && expected_to_sniff); |
| |
| // Simulate an OnResponseCompleted from the ResourceLoader. |
| if (should_be_blocked) { |
| if (!scenario.data().empty()) { |
| // TODO(nick): We may be left in an inconsistent state when blocking an |
| // empty nosniff response. Remove the above 'if'. |
| EXPECT_EQ(MockResourceLoader::Status::CANCELED, mock_loader_->status()); |
| } |
| net::URLRequestStatus status(net::URLRequestStatus::CANCELED, |
| net::ERR_ABORTED); |
| EXPECT_EQ(stream_sink_body_, ""); |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnResponseCompleted(status)); |
| } else { |
| EXPECT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->status()); |
| EXPECT_EQ(stream_sink_body_, scenario.data()); |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnResponseCompleted( |
| net::URLRequestStatus::FromError(net::OK))); |
| } |
| |
| // Ensure that all or none of the data arrived. |
| if (should_be_blocked) |
| EXPECT_EQ(stream_sink_body_, ""); |
| else |
| EXPECT_EQ(stream_sink_body_, scenario.data()); |
| |
| // 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 MimeType::kHtml: |
| bucket = "HTML"; |
| break; |
| case MimeType::kXml: |
| bucket = "XML"; |
| break; |
| case MimeType::kJson: |
| bucket = "JSON"; |
| break; |
| case MimeType::kPlain: |
| bucket = "Plain"; |
| break; |
| case MimeType::kOthers: |
| bucket = "Others"; |
| break; |
| case MimeType::kInvalidMimeType: |
| DCHECK_EQ(Verdict::kAllow, scenario.verdict); |
| DCHECK_EQ(-1, scenario.verdict_packet); |
| bucket = "No blocking = no bucket"; |
| break; |
| } |
| int start_action = static_cast<int>( |
| network::CrossOriginReadBlocking::Action::kResponseStarted); |
| int end_action = -1; |
| if (should_be_blocked && expected_to_sniff) { |
| end_action = static_cast<int>( |
| network::CrossOriginReadBlocking::Action::kBlockedAfterSniffing); |
| } else if (should_be_blocked && !expected_to_sniff) { |
| end_action = static_cast<int>( |
| network::CrossOriginReadBlocking::Action::kBlockedWithoutSniffing); |
| } else if (!should_be_blocked && expected_to_sniff) { |
| end_action = static_cast<int>( |
| network::CrossOriginReadBlocking::Action::kAllowedAfterSniffing); |
| } else if (!should_be_blocked && !expected_to_sniff) { |
| end_action = static_cast<int>( |
| network::CrossOriginReadBlocking::Action::kAllowedWithoutSniffing); |
| } else { |
| NOTREACHED(); |
| } |
| |
| // The parser-breaker detection only on responses that are not blocked by |
| // normal mime-sniffing. |
| bool scenario_requires_parser_breaker_detection = |
| (network::CrossOriginReadBlocking::kYes == |
| network::CrossOriginReadBlocking::SniffForFetchOnlyResource( |
| scenario.data())) && |
| !((network::CrossOriginReadBlocking::kYes == |
| network::CrossOriginReadBlocking::SniffForJSON(scenario.data())) && |
| (scenario.canonical_mime_type == MimeType::kJson || |
| scenario.canonical_mime_type == MimeType::kPlain)); |
| if (should_be_blocked && expected_to_sniff && |
| scenario_requires_parser_breaker_detection) { |
| expected_counts[histogram_base + ".BlockedForParserBreaker"] = 1; |
| } |
| |
| // 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) { |
| expected_counts[histogram_base + ".BytesReadForSniffing"] = 1; |
| |
| // Only the packets up to verdict_packet are sniffed. |
| int expected_sniff_bytes = 0; |
| for (int i = 0; i <= effective_verdict_packet; i++) { |
| expected_sniff_bytes += strlen(packets_vector[i]); |
| } |
| EXPECT_EQ( |
| 1, histograms.GetBucketCount(histogram_base + ".BytesReadForSniffing", |
| expected_sniff_bytes)); |
| } |
| if (should_be_blocked) { |
| expected_counts[histogram_base + ".Blocked"] = 1; |
| expected_counts[histogram_base + ".Blocked.ContentLength.WasAvailable"] = 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)); |
| |
| // Process all messages to ensure proper test teardown. |
| content::RunAllPendingInMessageLoop(); |
| } |
| |
| // 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, |
| false /* = inject_mime_sniffer */); |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnWillStart(request_->url())); |
| |
| // Set up response based on scenario. |
| scoped_refptr<network::ResourceResponse> response = CreateResponse( |
| scenario.response_content_type, scenario.include_no_sniff_header, |
| scenario.simulate_range_response, 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_packet != kVerdictPacketForHeadersBasedVerdict); |
| ASSERT_EQ(expected_to_sniff, document_blocker_->analyzer_->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. |
| size_t bytes_delivered = 0; |
| int buffer_requests = 0; |
| int packets = 0; |
| std::vector<const char*> packets_vector(scenario.packets); |
| packets_vector.push_back(""); |
| for (base::StringPiece packet : packets_vector) { |
| bool should_be_streaming = scenario.verdict == Verdict::kAllow && |
| packets > scenario.verdict_packet; |
| stream_sink_->set_defer_on_will_read(true); |
| mock_loader_->OnWillRead(); |
| if (bytes_delivered == 0 || should_be_streaming) { |
| ASSERT_EQ(++buffer_requests, stream_sink_->on_will_read_called()); |
| ASSERT_EQ(MockResourceLoader::Status::CALLBACK_PENDING, |
| mock_loader_->status()); |
| |
| // 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_->WaitUntilDeferred(); |
| stream_sink_->Resume(); |
| } else { |
| if (document_blocker_->blocked_read_completed_) { |
| // We've decided to block. |
| EXPECT_EQ(Verdict::kBlock, scenario.verdict); |
| EXPECT_EQ(MockResourceLoader::Status::CANCELED, mock_loader_->status()); |
| break; |
| } else { |
| EXPECT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->status()); |
| } |
| } |
| ASSERT_NE(nullptr, mock_loader_->io_buffer()); |
| |
| if (!should_be_streaming) { |
| ASSERT_NE(nullptr, document_blocker_->local_buffer_.get()); |
| EXPECT_EQ(mock_loader_->io_buffer()->data(), |
| document_blocker_->local_buffer_->data() + bytes_delivered) |
| << "Should have used a different IOBuffer for sniffing"; |
| } else { |
| EXPECT_EQ(nullptr, document_blocker_->local_buffer_.get()); |
| EXPECT_EQ(mock_loader_->io_buffer(), stream_sink_->buffer()) |
| << "Should have used original IOBuffer when sniffing not needed"; |
| } |
| // Deliver the next packet of the response body. |
| mock_loader_->OnReadCompleted(packet); |
| if (packet.empty() && (packets == scenario.verdict_packet) && |
| (bytes_delivered > 0)) { |
| // This case will result in CrossSiteDocumentResourceHandler having to |
| // synthesize an extra OnWillRead. |
| stream_sink_->set_defer_on_will_read(true); |
| stream_sink_->WaitUntilDeferred(); |
| stream_sink_->Resume(); |
| } |
| if (mock_loader_->status() == |
| MockResourceLoader::Status::CALLBACK_PENDING) { |
| // CALLBACK_PENDING is only expected in the case when streaming starts. |
| if (scenario.verdict_packet == kVerdictPacketForHeadersBasedVerdict) { |
| // If not sniffing, then |
| // - if response is allowed, then streaming should start in |
| // OnResponseStarted (and we shouldn't hit CALLBACK_PENDING state). |
| // - if response is blocked, then CALLBACK_PENDING state will happen |
| // when enforcing blocking - at packet #0. |
| EXPECT_EQ(Verdict::kBlock, scenario.verdict); |
| EXPECT_EQ(0, packets); |
| } else { |
| // If sniffing, then streaming should start at the verdict packet. |
| EXPECT_EQ(scenario.verdict_packet, packets); |
| } |
| mock_loader_->WaitUntilIdleOrCanceled(); |
| } |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, mock_loader_->status()); |
| |
| if (document_blocker_->blocked_read_completed_) { |
| EXPECT_EQ(Verdict::kBlock, scenario.verdict); |
| ASSERT_EQ(packets, std::max(0, scenario.verdict_packet)); |
| } |
| |
| bytes_delivered += packet.size(); |
| packets++; |
| |
| EXPECT_EQ(nullptr, mock_loader_->io_buffer()); |
| } |
| |
| // Verify that the response is blocked or allowed as expected. |
| if (scenario.verdict == Verdict::kBlock) { |
| 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 { |
| // Make sure that the response was delivered. |
| EXPECT_EQ(scenario.data(), stream_sink_body_) |
| << "Response should have been delivered to the renderer."; |
| EXPECT_FALSE(document_blocker_->blocked_read_completed_); |
| EXPECT_EQ(expected_to_sniff, document_blocker_->allow_based_on_sniffing_); |
| } |
| |
| // Process all messages to ensure proper test teardown. |
| content::RunAllPendingInMessageLoop(); |
| } |
| |
| // Runs a particular TestScenario (passed as the test's parameter) through the |
| // ResourceLoader, MimeSniffingResourceHandler and |
| // CrossSiteDocumentResourceHandler, verifying that the response is correctly |
| // allowed or blocked based on the scenario. |
| TEST_P(CrossSiteDocumentResourceHandlerTest, MimeSnifferInterop) { |
| 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, |
| true /* = inject_mime_sniffer */); |
| base::HistogramTester histograms; |
| |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnWillStart(request_->url())); |
| |
| // Set up response based on scenario. |
| scoped_refptr<network::ResourceResponse> response = CreateResponse( |
| scenario.response_content_type, scenario.include_no_sniff_header, |
| scenario.simulate_range_response, scenario.cors_response, |
| scenario.initiator_origin); |
| |
| // Call OnResponseStarted. Note that MimeSniffingResourceHandler will not |
| // immediately forward the call to CrossSiteDocumentResourceHandler. |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnResponseStarted(response)); |
| |
| // Calculate expectations, based on scenario properties. |
| bool expected_to_sniff = scenario.verdict_packet >= 0; |
| bool should_be_blocked = scenario.verdict == Verdict::kBlock; |
| bool expected_to_block_based_on_headers = |
| expected_to_sniff || should_be_blocked; |
| |
| // Push all packets through. Minimal verifications, because we don't want the |
| // test to make assumptions about when exactly MimeSniffingResourceHandler |
| // forwards the calls down to CrossSiteDocumentResourceHandler |
| std::vector<const char*> packets_vector(scenario.packets); |
| packets_vector.push_back(""); // End-of-stream is marked by an empty packet. |
| for (int packet_index = 0; |
| packet_index < static_cast<int>(packets_vector.size()); packet_index++) { |
| const base::StringPiece packet = packets_vector[packet_index]; |
| SCOPED_TRACE(testing::Message() |
| << "While delivering packet #" << packet_index); |
| mock_loader_->OnWillRead(); |
| if (mock_loader_->status() == MockResourceLoader::Status::CANCELED) |
| break; |
| if (mock_loader_->status() == MockResourceLoader::Status::CALLBACK_PENDING) |
| mock_loader_->WaitUntilIdleOrCanceled(); |
| |
| mock_loader_->OnReadCompleted(packet); |
| if (mock_loader_->status() == MockResourceLoader::Status::CANCELED) |
| break; |
| if (mock_loader_->status() == MockResourceLoader::Status::CALLBACK_PENDING) |
| mock_loader_->WaitUntilIdleOrCanceled(); |
| } |
| |
| // Call OnResponseCompleted. |
| net::URLRequestStatus request_status; |
| if (mock_loader_->status() == MockResourceLoader::Status::CANCELED) { |
| request_status = net::URLRequestStatus(net::URLRequestStatus::CANCELED, |
| net::ERR_ABORTED); |
| } else { |
| request_status = net::URLRequestStatus::FromError(net::OK); |
| } |
| ASSERT_EQ(MockResourceLoader::Status::IDLE, |
| mock_loader_->OnResponseCompleted(request_status)); |
| |
| // At this point MimeSniffingResourceHandler should have forwarded all calls |
| // down to CrossSiteDocumentResourceHandler - it is now okay to verify |
| // CrossSiteDocumentResourceHandler's behavior. |
| ASSERT_TRUE(document_blocker_->has_response_started_); |
| EXPECT_EQ(scenario.canonical_mime_type, |
| document_blocker_->analyzer_->canonical_mime_type()); |
| EXPECT_EQ(expected_to_sniff, document_blocker_->analyzer_->needs_sniffing()); |
| ASSERT_EQ(expected_to_block_based_on_headers, |
| document_blocker_->should_block_based_on_headers_); |
| |
| // Ensure that all or none of the data arrived. |
| if (should_be_blocked) |
| EXPECT_EQ("", stream_sink_body_); |
| else |
| EXPECT_EQ(scenario.data(), stream_sink_body_); |
| |
| // Process all messages to ensure proper test teardown. |
| content::RunAllPendingInMessageLoop(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| CrossSiteDocumentResourceHandlerTest, |
| ::testing::ValuesIn(kScenarios)); |
| |
| } // namespace content |