blob: 08e7a4ef28ca9e45896af50c5a16c5a7b66c128d [file] [log] [blame]
// Copyright 2018 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 <utility>
#include <vector>
#include "base/strings/string_piece.h"
#include "net/http/http_util.h"
#include "services/network/cross_origin_read_blocking.h"
#include "services/network/public/cpp/resource_response_info.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::StringPiece;
using MimeType = network::CrossOriginReadBlocking::MimeType;
using SniffingResult = network::CrossOriginReadBlocking::SniffingResult;
namespace network {
TEST(CrossOriginReadBlockingTest, IsBlockableScheme) {
GURL data_url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA==");
GURL ftp_url("ftp://google.com");
GURL mailto_url("mailto:google@google.com");
GURL about_url("about:chrome");
GURL http_url("http://google.com");
GURL https_url("https://google.com");
EXPECT_FALSE(CrossOriginReadBlocking::IsBlockableScheme(data_url));
EXPECT_FALSE(CrossOriginReadBlocking::IsBlockableScheme(ftp_url));
EXPECT_FALSE(CrossOriginReadBlocking::IsBlockableScheme(mailto_url));
EXPECT_FALSE(CrossOriginReadBlocking::IsBlockableScheme(about_url));
EXPECT_TRUE(CrossOriginReadBlocking::IsBlockableScheme(http_url));
EXPECT_TRUE(CrossOriginReadBlocking::IsBlockableScheme(https_url));
}
TEST(CrossOriginReadBlockingTest, IsValidCorsHeaderSet) {
url::Origin frame_origin = url::Origin::Create(GURL("http://www.google.com"));
EXPECT_TRUE(CrossOriginReadBlocking::IsValidCorsHeaderSet(frame_origin, "*"));
EXPECT_FALSE(
CrossOriginReadBlocking::IsValidCorsHeaderSet(frame_origin, "\"*\""));
EXPECT_FALSE(CrossOriginReadBlocking::IsValidCorsHeaderSet(
frame_origin, "http://mail.google.com"));
EXPECT_TRUE(CrossOriginReadBlocking::IsValidCorsHeaderSet(
frame_origin, "http://www.google.com"));
EXPECT_FALSE(CrossOriginReadBlocking::IsValidCorsHeaderSet(
frame_origin, "https://www.google.com"));
EXPECT_FALSE(CrossOriginReadBlocking::IsValidCorsHeaderSet(
frame_origin, "http://yahoo.com"));
EXPECT_FALSE(CrossOriginReadBlocking::IsValidCorsHeaderSet(frame_origin,
"www.google.com"));
}
TEST(CrossOriginReadBlockingTest, SniffForHTML) {
using CORB = CrossOriginReadBlocking;
// Something that technically matches the start of a valid HTML tag.
EXPECT_EQ(SniffingResult::kYes,
CORB::SniffForHTML(" \t\r\n <HtMladfokadfkado"));
// HTML comment followed by whitespace and valid HTML tags.
EXPECT_EQ(SniffingResult::kYes,
CORB::SniffForHTML(" <!-- this is comment -->\n<html><body>"));
// HTML comment, whitespace, more HTML comments, HTML tags.
EXPECT_EQ(
SniffingResult::kYes,
CORB::SniffForHTML(
"<!-- this is comment -->\n<!-- this is comment -->\n<html><body>"));
// HTML comment followed by valid HTML tag.
EXPECT_EQ(
SniffingResult::kYes,
CORB::SniffForHTML("<!-- this is comment <!-- -->\n<script></script>"));
// Whitespace followed by valid Javascript.
EXPECT_EQ(SniffingResult::kNo,
CORB::SniffForHTML(" var name=window.location;\nadfadf"));
// HTML comment followed by valid Javascript.
EXPECT_EQ(
SniffingResult::kNo,
CORB::SniffForHTML(
" <!-- this is comment\n document.write(1);\n// -->\nwindow.open()"));
// HTML/Javascript polyglot should return kNo.
EXPECT_EQ(SniffingResult::kNo,
CORB::SniffForHTML(
"<!--/*--><html><body><script type='text/javascript'><!--//*/\n"
"var blah = 123;\n"
"//--></script></body></html>"));
// Tests to cover more of MaybeSkipHtmlComment.
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML("<!-- -/* --><html>"));
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML("<!-- --/* --><html>"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- -/* -->\n<html>"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- --/* -->\n<html>"));
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML("<!----> <html>"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!---->\n<html>"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!---->\r<html>"));
EXPECT_EQ(SniffingResult::kYes,
CORB::SniffForHTML("<!-- ---/-->\n<html><body>"));
// HTML spec only allows *ASCII* whitespace before the first html element.
// See also https://html.spec.whatwg.org/multipage/syntax.html and
// https://infra.spec.whatwg.org/#ascii-whitespace.
EXPECT_EQ(SniffingResult::kNo, CORB::SniffForHTML("<!---->\u2028<html>"));
EXPECT_EQ(SniffingResult::kNo, CORB::SniffForHTML("<!---->\u2029<html>"));
// Order of line terminators.
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- -->\n<b>\rx"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- -->\r<b>\nx"));
EXPECT_EQ(SniffingResult::kNo, CORB::SniffForHTML("<!-- -->\nx\r<b>"));
EXPECT_EQ(SniffingResult::kNo, CORB::SniffForHTML("<!-- -->\rx\n<b>"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- -->\n<b>\u2028x"));
EXPECT_EQ(SniffingResult::kNo, CORB::SniffForHTML("<!-- -->\u2028<b>\n<b>"));
// In UTF8 encoding <LS> is 0xE2 0x80 0xA8 and <PS> is 0xE2 0x80 0xA9.
// Let's verify that presence of 0xE2 alone doesn't throw
// FindFirstJavascriptLineTerminator into an infinite loop.
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- --> \xe2 \n<b"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- --> \xe2\x80 \n<b"));
EXPECT_EQ(SniffingResult::kYes, CORB::SniffForHTML("<!-- --> \x80 \n<b"));
// Commented out html tag followed by non-html (" x").
StringPiece commented_out_html_tag_data("<!-- <html> <?xml> \n<html>-->\nx");
EXPECT_EQ(SniffingResult::kNo,
CORB::SniffForHTML(commented_out_html_tag_data));
// Prefixes of |commented_out_html_tag_data| should be indeterminate.
// This covers testing "<!-" as well as "<!-- not terminated yet...".
StringPiece almost_html = commented_out_html_tag_data;
while (!almost_html.empty()) {
almost_html.remove_suffix(1);
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML(almost_html))
<< almost_html;
}
// Explicit tests for an unfinished comment (some also covered by the prefix
// tests above).
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML(""));
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML("<!"));
EXPECT_EQ(SniffingResult::kMaybe, CORB::SniffForHTML("<!-- unterminated..."));
EXPECT_EQ(SniffingResult::kMaybe,
CORB::SniffForHTML("<!-- blah --> <html> no newline yet"));
}
TEST(CrossOriginReadBlockingTest, SniffForXML) {
StringPiece xml_data(" \t \r \n <?xml version=\"1.0\"?>\n <catalog");
StringPiece non_xml_data(" var name=window.location;\nadfadf");
StringPiece empty_data("");
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForXML(xml_data));
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForXML(non_xml_data));
// Empty string should be indeterminate.
EXPECT_EQ(SniffingResult::kMaybe,
CrossOriginReadBlocking::SniffForXML(empty_data));
}
TEST(CrossOriginReadBlockingTest, SniffForJSON) {
StringPiece json_data("\t\t\r\n { \"name\" : \"chrome\", ");
StringPiece json_corrupt_after_first_key(
"\t\t\r\n { \"name\" :^^^^!!@#\1\", ");
StringPiece json_data2("{ \"key \\\" \" \t\t\r\n:");
StringPiece non_json_data0("\t\t\r\n { name : \"chrome\", ");
StringPiece non_json_data1("\t\t\r\n foo({ \"name\" : \"chrome\", ");
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForJSON(json_data));
EXPECT_EQ(SniffingResult::kYes, CrossOriginReadBlocking::SniffForJSON(
json_corrupt_after_first_key));
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForJSON(json_data2));
// All prefixes prefixes of |json_data2| ought to be indeterminate.
StringPiece almost_json = json_data2;
while (!almost_json.empty()) {
almost_json.remove_suffix(1);
EXPECT_EQ(SniffingResult::kMaybe,
CrossOriginReadBlocking::SniffForJSON(almost_json))
<< almost_json;
}
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON(non_json_data0));
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON(non_json_data1));
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForJSON(R"({"" : 1})"))
<< "Empty strings are accepted";
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON(R"({'' : 1})"))
<< "Single quotes are not accepted";
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForJSON("{\"\\\"\" : 1}"))
<< "Escaped quotes are recognized";
EXPECT_EQ(SniffingResult::kYes,
CrossOriginReadBlocking::SniffForJSON(R"({"\\\u000a" : 1})"))
<< "Escaped control characters are recognized";
EXPECT_EQ(SniffingResult::kMaybe,
CrossOriginReadBlocking::SniffForJSON(R"({"\\\u00)"))
<< "Incomplete escape results in maybe";
EXPECT_EQ(SniffingResult::kMaybe,
CrossOriginReadBlocking::SniffForJSON("{\"\\"))
<< "Incomplete escape results in maybe";
EXPECT_EQ(SniffingResult::kMaybe,
CrossOriginReadBlocking::SniffForJSON("{\"\\\""))
<< "Incomplete escape results in maybe";
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON("{\"\n\" : true}"))
<< "Unescaped control characters are rejected";
EXPECT_EQ(SniffingResult::kNo, CrossOriginReadBlocking::SniffForJSON("{}"))
<< "Empty dictionary is not recognized (since it's valid JS too)";
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON("[true, false, 1, 2]"))
<< "Lists dictionary are not recognized (since they're valid JS too)";
EXPECT_EQ(SniffingResult::kNo,
CrossOriginReadBlocking::SniffForJSON(R"({":"})"))
<< "A colon character inside a string does not trigger a match";
}
TEST(CrossOriginReadBlockingTest, GetCanonicalMimeType) {
std::vector<std::pair<const char*, MimeType>> tests = {
// Basic tests for things in the original implementation:
{"text/html", MimeType::kHtml},
{"text/xml", MimeType::kXml},
{"application/rss+xml", MimeType::kXml},
{"application/xml", MimeType::kXml},
{"application/json", MimeType::kJson},
{"text/json", MimeType::kJson},
{"text/plain", MimeType::kPlain},
// Other mime types:
{"application/foobar", MimeType::kOthers},
// Regression tests for https://crbug.com/799155 (prefix/suffix matching):
{"application/activity+json", MimeType::kJson},
{"text/foobar+xml", MimeType::kXml},
// No match without a '+' character:
{"application/jsonfoobar", MimeType::kOthers},
{"application/foobarjson", MimeType::kOthers},
{"application/xmlfoobar", MimeType::kOthers},
{"application/foobarxml", MimeType::kOthers},
// Case-insensitive comparison:
{"APPLICATION/JSON", MimeType::kJson},
{"APPLICATION/ACTIVITY+JSON", MimeType::kJson},
{"appLICAtion/zIP", MimeType::kNeverSniffed},
// Images are allowed cross-site, and SVG is an image, so we should
// classify SVG as "other" instead of "xml" (even though it technically is
// an xml document). Same argument for DASH video format.
{"image/svg+xml", MimeType::kOthers},
{"application/dash+xml", MimeType::kOthers},
// Javascript should not be blocked.
{"application/javascript", MimeType::kOthers},
{"application/jsonp", MimeType::kOthers},
// TODO(lukasza): Remove in the future, once this MIME type is not used in
// practice. See also https://crbug.com/826756#c3
{"application/json+protobuf", MimeType::kJson},
{"APPLICATION/JSON+PROTOBUF", MimeType::kJson},
// According to specs, these types are not XML or JSON. See also:
// - https://mimesniff.spec.whatwg.org/#xml-mime-type
// - https://mimesniff.spec.whatwg.org/#json-mime-type
{"text/x-json", MimeType::kOthers},
{"text/json+blah", MimeType::kOthers},
{"application/json+blah", MimeType::kOthers},
{"text/xml+blah", MimeType::kOthers},
{"application/xml+blah", MimeType::kOthers},
// Types protected without sniffing.
{"application/gzip", MimeType::kNeverSniffed},
{"application/x-protobuf", MimeType::kNeverSniffed},
{"application/x-gzip", MimeType::kNeverSniffed},
{"application/x-www-form-urlencoded", MimeType::kNeverSniffed},
{"application/zip", MimeType::kNeverSniffed},
{"multipart/byteranges", MimeType::kNeverSniffed},
{"text/event-stream", MimeType::kNeverSniffed},
// TODO(lukasza): https://crbug.com/944162: Add application/pdf and
// text/csv to the list of content types tested here (after
// kMimeHandlerViewInCrossProcessFrame gets enabled by default).
};
for (const auto& test : tests) {
const char* input = test.first; // e.g. "text/html"
MimeType expected = test.second;
MimeType actual = CrossOriginReadBlocking::GetCanonicalMimeType(input);
EXPECT_EQ(expected, actual)
<< "when testing with the following input: " << input;
}
}
network::ResourceResponseInfo CreateResponse(std::string raw_headers) {
scoped_refptr<net::HttpResponseHeaders> headers =
base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(raw_headers));
network::ResourceResponseInfo response = {};
response.headers = headers;
return response;
}
TEST(CrossOriginReadBlockingTest, SeemsSensitiveFromCORSHeuristic) {
// Response with no CORS header.
network::ResourceResponseInfo no_cors_response =
CreateResponse("HTTP/1.1 200 OK");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(no_cors_response));
// Response with CORS value = "*", so not sensitive.
network::ResourceResponseInfo cors_any_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Access-Control-Allow-Origin: *");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(cors_any_response));
// Response with CORS value = "null", so not sensitive.
network::ResourceResponseInfo cors_null_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Access-Control-Allow-Origin: null");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(cors_null_response));
// Response with CORS header restricting access to a particular origin.
network::ResourceResponseInfo cors_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Access-Control-Allow-Origin: http://www.a.com/");
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(cors_response));
}
TEST(CrossOriginReadBlockingTest, SeemsSensitiveFromCacheHeuristic) {
// Response with no cache-control or vary header.
network::ResourceResponseInfo no_cache_response =
CreateResponse("HTTP/1.1 200 OK");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(no_cache_response));
// Response with cache-control but no vary header.
network::ResourceResponseInfo cache_only_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Cache-Control: private");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(cache_only_response));
// Response with vary: origin but no cache-control header.
network::ResourceResponseInfo vary_only_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: origin");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(vary_only_response));
// Response with vary: user-agent and cache-control: no-cache (should still
// not seem sensitive).
network::ResourceResponseInfo wrong_values_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: user-agent\n"
"Cache-Control: no-cache");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(wrong_values_response));
// Response with vary: origin and cache-control: private.
network::ResourceResponseInfo vary_and_cache_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: origin\n"
"Cache-Control: private");
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(vary_and_cache_response));
// Response with vary: origin, user-agent and cache-control: private, no-store
// (we should still find the relevant values).
network::ResourceResponseInfo extra_values_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: origin, user-agent\n"
"Cache-Control: no-cache, private");
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(extra_values_response));
}
TEST(CrossOriginReadBlockingTest, SeemsSensitiveWithBothHeuristics) {
// Response with CORS heuristic should not appear sensitive to cache
// heuristic.
network::ResourceResponseInfo cors_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Access-Control-Allow-Origin: http://www.a.com/");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(cors_response));
// Response with cache heuristic should not appear sensitive to CORS
// heuristic.
network::ResourceResponseInfo cache_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: origin\n"
"Cache-Control: private");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(cache_response));
// Response with both cache and CORS heuristic signals (e.g. vary: origin +
// cache-control: private as well as the access-control-allow-origin header).
network::ResourceResponseInfo both_response = CreateResponse(
"HTTP/1.1 200 OK\n"
"Vary: origin\n"
"Cache-Control: private\n"
"Access-Control-Allow-Origin: http://www.a.com/");
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCORSHeuristic(both_response));
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::
SeemsSensitiveFromCacheHeuristic(both_response));
}
TEST(CrossOriginReadBlockingTest, SupportsRangeRequests) {
// Response with no Accept-Ranges header. Should return false.
network::ResourceResponseInfo no_accept_ranges =
CreateResponse("HTTP/1.1 200 OK");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::SupportsRangeRequests(
no_accept_ranges));
// Response with an Accept-Ranges header. Should return true.
network::ResourceResponseInfo bytes_accept_ranges = CreateResponse(
"HTTP/1.1 200 OK\n"
"Accept-Ranges: bytes");
EXPECT_TRUE(CrossOriginReadBlocking::ResponseAnalyzer::SupportsRangeRequests(
bytes_accept_ranges));
// Response with an Accept-Ranges header value of |none|. Should return false.
network::ResourceResponseInfo none_accept_ranges = CreateResponse(
"HTTP/1.1 200 OK\n"
"Accept-Ranges: none");
EXPECT_FALSE(CrossOriginReadBlocking::ResponseAnalyzer::SupportsRangeRequests(
none_accept_ranges));
}
} // namespace network