blob: 43a06b123d83892550b862437d025c1391c28809 [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 "services/network/cross_origin_read_blocking.h"
#include <stddef.h>
#include <algorithm>
#include <set>
#include <string>
#include <unordered_set>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "net/base/mime_sniffer.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/url_request.h"
#include "services/network/cross_origin_resource_policy.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_response.h"
#include "services/network/public/cpp/resource_response_info.h"
using base::StringPiece;
using MimeType = network::CrossOriginReadBlocking::MimeType;
using SniffingResult = network::CrossOriginReadBlocking::SniffingResult;
namespace network {
namespace {
// MIME types
const char kTextHtml[] = "text/html";
const char kTextXml[] = "text/xml";
const char kAppXml[] = "application/xml";
const char kAppJson[] = "application/json";
const char kImageSvg[] = "image/svg+xml";
const char kTextJson[] = "text/json";
const char kTextPlain[] = "text/plain";
// TODO(lukasza): Remove kJsonProtobuf once this MIME type is not used in
// practice. See also
const char kJsonProtobuf[] = "application/json+protobuf";
// MIME type suffixes
const char kJsonSuffix[] = "+json";
const char kXmlSuffix[] = "+xml";
void AdvancePastWhitespace(StringPiece* data) {
size_t offset = data->find_first_not_of(" \t\r\n");
if (offset == base::StringPiece::npos) {
// |data| was entirely whitespace.
} else {
// Returns kYes if |data| starts with one of the string patterns in
// |signatures|, kMaybe if |data| is a prefix of one of the patterns in
// |signatures|, and kNo otherwise.
// When kYes is returned, the matching prefix is erased from |data|.
SniffingResult MatchesSignature(StringPiece* data,
const StringPiece signatures[],
size_t arr_size,
base::CompareCase compare_case) {
for (size_t i = 0; i < arr_size; ++i) {
if (signatures[i].length() <= data->length()) {
if (base::StartsWith(*data, signatures[i], compare_case)) {
// When |signatures[i]| is a prefix of |data|, it constitutes a match.
// Strip the matching characters, and return.
return CrossOriginReadBlocking::kYes;
} else {
if (base::StartsWith(signatures[i], *data, compare_case)) {
// When |data| is a prefix of |signatures[i]|, that means that
// subsequent bytes in the stream could cause a match to occur.
return CrossOriginReadBlocking::kMaybe;
return CrossOriginReadBlocking::kNo;
size_t FindFirstJavascriptLineTerminator(const base::StringPiece& hay,
size_t pos) {
// defines LineTerminator ::= <LF> | <CR> | <LS> | <PS>.
// defines <LF>, <CR>, <LS> ::= "\u2028", <PS> ::= "\u2029".
// In UTF8 encoding <LS> is 0xE2 0x80 0xA8 and <PS> is 0xE2 0x80 0xA9.
while (true) {
pos = hay.find_first_of("\n\r\xe2", pos);
if (pos == base::StringPiece::npos)
if (hay[pos] != '\xe2') {
DCHECK(hay[pos] == '\r' || hay[pos] == '\n');
// TODO(lukasza): Prevent matching 3 bytes that span/straddle 2 UTF8
// characters.
base::StringPiece substr = hay.substr(pos);
if (substr.starts_with("\u2028") || substr.starts_with("\u2029"))
pos++; // Skip the \xe2 character.
return pos;
// Checks if |data| starts with an HTML comment (i.e. with "<!-- ... -->").
// - If there is a valid, terminated comment then returns kYes.
// - If there is a start of a comment, but the comment is not completed (e.g.
// |data| == "<!-" or |data| == "<!-- not terminated yet") then returns
// kMaybe.
// - Returns kNo otherwise.
// Mutates |data| to advance past the comment when returning kYes. Note that
// SingleLineHTMLCloseComment ECMAscript rule is taken into account which means
// that characters following an HTML comment are consumed up to the nearest line
// terminating character.
SniffingResult MaybeSkipHtmlComment(StringPiece* data) {
constexpr StringPiece kStartString = "<!--";
if (!data->starts_with(kStartString)) {
if (kStartString.starts_with(*data))
return CrossOriginReadBlocking::kMaybe;
return CrossOriginReadBlocking::kNo;
constexpr StringPiece kEndString = "-->";
size_t end_of_html_comment = data->find(kEndString, kStartString.length());
if (end_of_html_comment == StringPiece::npos)
return CrossOriginReadBlocking::kMaybe;
end_of_html_comment += kEndString.length();
// Skipping until the first line terminating character. See
// for the motivation behind this.
size_t end_of_line =
FindFirstJavascriptLineTerminator(*data, end_of_html_comment);
if (end_of_line == base::StringPiece::npos)
return CrossOriginReadBlocking::kMaybe;
// Found real end of the combined HTML/JS comment.
return CrossOriginReadBlocking::kYes;
// Removes headers that should be blocked in cross-origin case.
// Note that corbSanitizedResponse in
// has an empty list of headers, but the code below doesn't remove all the
// headers for improved user experience - for better error messages for CORS.
// See also and
// the http/tests/xmlhttprequest/origin-exact-matching/07.html layout test.
// Note that CORB doesn't block responses allowed through CORS - this means
// that the list of allowed headers below doesn't have to consider header
// names listed in the Access-Control-Expose-Headers header.
void BlockResponseHeaders(
const scoped_refptr<net::HttpResponseHeaders>& headers) {
std::unordered_set<std::string> names_of_headers_to_remove;
size_t it = 0;
std::string name;
std::string value;
while (headers->EnumerateHeaderLines(&it, &name, &value)) {
// Don't remove CORS headers - doing so would lead to incorrect error
// messages for CORS-blocked responses (e.g. Blink would say "[...] No
// 'Access-Control-Allow-Origin' header is present [...]" instead of saying
// something like "[...] Access-Control-Allow-Origin' header has a value
// 'http://www2.localhost:8000' that is not equal to the supplied origin
// [...]").
if (base::StartsWith(name, "Access-Control-",
base::CompareCase::INSENSITIVE_ASCII)) {
// Remove all other headers.
std::set<int>& GetPluginProxyingProcesses() {
static base::NoDestructor<std::set<int>> set;
return *set;
bool ShouldEnforceInitiatorLock() {
return base::FeatureList::IsEnabled(network::features::kNetworkService) &&
void RecordCorbResultVsInitiatorLockCompatibility(
CrossOriginReadBlocking::CorbResultVsInitiatorLockCompatibility value) {
if (ShouldEnforceInitiatorLock()) {
"SiteIsolation.XSD.NetworkService.InitiatorLockCompatibility", value);
} // namespace
MimeType CrossOriginReadBlocking::GetCanonicalMimeType(
base::StringPiece mime_type) {
// Checking for image/svg+xml early ensures that it won't get classified as
// MimeType::kXml by the presence of the "+xml"
// suffix.
if (base::LowerCaseEqualsASCII(mime_type, kImageSvg))
return MimeType::kOthers;
// See also
if (base::LowerCaseEqualsASCII(mime_type, kTextHtml))
return MimeType::kHtml;
// See also
constexpr auto kCaseInsensitive = base::CompareCase::INSENSITIVE_ASCII;
if (base::LowerCaseEqualsASCII(mime_type, kAppJson) ||
base::LowerCaseEqualsASCII(mime_type, kTextJson) ||
base::LowerCaseEqualsASCII(mime_type, kJsonProtobuf) ||
base::EndsWith(mime_type, kJsonSuffix, kCaseInsensitive)) {
return MimeType::kJson;
// See also
if (base::LowerCaseEqualsASCII(mime_type, kAppXml) ||
base::LowerCaseEqualsASCII(mime_type, kTextXml) ||
base::EndsWith(mime_type, kXmlSuffix, kCaseInsensitive)) {
return MimeType::kXml;
if (base::LowerCaseEqualsASCII(mime_type, kTextPlain))
return MimeType::kPlain;
return MimeType::kOthers;
bool CrossOriginReadBlocking::IsBlockableScheme(const GURL& url) {
// We exclude ftp:// from here. FTP doesn't provide a Content-Type
// header which our policy depends on, so we cannot protect any
// response from FTP servers.
return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme);
bool CrossOriginReadBlocking::IsValidCorsHeaderSet(
const url::Origin& frame_origin,
const std::string& access_control_origin) {
// Many websites are sending back "\"*\"" instead of "*". This is
// non-standard practice, and not supported by Chrome. Refer to
// CrossOriginAccessControl::passesAccessControlCheck().
// Note that "null" offers no more protection than "*" because it matches any
// unique origin, such as data URLs. Any origin can thus access it, so don't
// bother trying to block this case.
// TODO(dsjang): * is not allowed for the response from a request
// with cookies. This allows for more than what the renderer will
// eventually be able to receive, so we won't see illegal cross-site
// documents allowed by this. We have to find a way to see if this
// response is from a cookie-tagged request or not in the future.
if (access_control_origin == "*" || access_control_origin == "null")
return true;
return frame_origin.IsSameOriginWith(
// This function is a slight modification of |net::SniffForHTML|.
SniffingResult CrossOriginReadBlocking::SniffForHTML(StringPiece data) {
// The content sniffers used by Chrome and Firefox are using "<!--" as one of
// the HTML signatures, but it also appears in valid JavaScript, considered as
// well-formed JS by the browser. Since we do not want to block any JS, we
// exclude it from our HTML signatures. This can weaken our CORB policy,
// but we can break less websites.
// Note that <body> and <br> are not included below, since <b is a prefix of
// them.
// TODO(dsjang): parameterize |net::SniffForHTML| with an option that decides
// whether to include <!-- or not, so that we can remove this function.
// TODO(dsjang): Once CrossOriginReadBlocking is moved into the browser
// process, we should do single-thread checking here for the static
// initializer.
static const StringPiece kHtmlSignatures[] = {
StringPiece("<!doctype html"), // HTML5 spec
StringPiece("<script"), // HTML5 spec, Mozilla
StringPiece("<html"), // HTML5 spec, Mozilla
StringPiece("<head"), // HTML5 spec, Mozilla
StringPiece("<iframe"), // Mozilla
StringPiece("<h1"), // Mozilla
StringPiece("<div"), // Mozilla
StringPiece("<font"), // Mozilla
StringPiece("<table"), // Mozilla
StringPiece("<a"), // Mozilla
StringPiece("<style"), // Mozilla
StringPiece("<title"), // Mozilla
StringPiece("<b"), // Mozilla (note: subsumes <body>, <br>)
StringPiece("<p") // Mozilla
while (data.length() > 0) {
SniffingResult signature_match =
MatchesSignature(&data, kHtmlSignatures, base::size(kHtmlSignatures),
if (signature_match != kNo)
return signature_match;
SniffingResult comment_match = MaybeSkipHtmlComment(&data);
if (comment_match != kYes)
return comment_match;
// All of |data| was consumed, without a clear determination.
return kMaybe;
SniffingResult CrossOriginReadBlocking::SniffForXML(base::StringPiece data) {
// TODO(dsjang): Once CrossOriginReadBlocking is moved into the browser
// process, we should do single-thread checking here for the static
// initializer.
static const StringPiece kXmlSignatures[] = {StringPiece("<?xml")};
return MatchesSignature(&data, kXmlSignatures, base::size(kXmlSignatures),
SniffingResult CrossOriginReadBlocking::SniffForJSON(base::StringPiece data) {
// Currently this function looks for an opening brace ('{'), followed by a
// double-quoted string literal, followed by a colon. Importantly, such a
// sequence is a Javascript syntax error: although the JSON object syntax is
// exactly Javascript's object-initializer syntax, a Javascript object-
// initializer expression is not valid as a standalone Javascript statement.
// TODO(nick): We have to come up with a better way to sniff JSON. The
// following are known limitations of this function:
// Support non-dictionary values (e.g. lists)
enum {
} state = kStartState;
for (size_t i = 0; i < data.length(); ++i) {
const char c = data[i];
if (state != kLeftQuoteState && state != kEscapeState) {
// Whitespace is ignored (outside of string literals)
if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
} else {
// Inside string literals, control characters should result in rejection.
if ((c >= 0 && c < 32) || c == 127)
return kNo;
switch (state) {
case kStartState:
if (c == '{')
state = kLeftBraceState;
return kNo;
case kLeftBraceState:
if (c == '"')
state = kLeftQuoteState;
return kNo;
case kLeftQuoteState:
if (c == '"')
state = kRightQuoteState;
else if (c == '\\')
state = kEscapeState;
case kEscapeState:
// Simplification: don't bother rejecting hex escapes.
state = kLeftQuoteState;
case kRightQuoteState:
if (c == ':')
return kYes;
return kNo;
return kMaybe;
SniffingResult CrossOriginReadBlocking::SniffForFetchOnlyResource(
base::StringPiece data) {
// kScriptBreakingPrefixes contains prefixes that are conventionally used to
// prevent a JSON response from becoming a valid Javascript program (an attack
// vector known as XSSI). The presence of such a prefix is a strong signal
// that the resource is meant to be consumed only by the fetch API or
// XMLHttpRequest, and is meant to be protected from use in non-CORS, cross-
// origin contexts like <script>, <img>, etc.
// These prefixes work either by inducing a syntax error, or inducing an
// infinite loop. In either case, the prefix must create a guarantee that no
// matter what bytes follow it, the entire response would be worthless to
// execute as a <script>.
static const StringPiece kScriptBreakingPrefixes[] = {
// Parser breaker prefix.
// Built into angular.js (followed by a comma and a newline):
// Built into the Java Spring framework (followed by a comma and a space):
// Observed on (without a comma, followed by a newline).
// Apache struts:
// Spring framework (historically):
StringPiece("{} &&"),
// Infinite loops.
StringPiece("for(;;);"), // observed on
StringPiece("while(1);"), StringPiece("for (;;);"),
StringPiece("while (1);"),
SniffingResult has_parser_breaker = MatchesSignature(
&data, kScriptBreakingPrefixes, base::size(kScriptBreakingPrefixes),
if (has_parser_breaker != kNo)
return has_parser_breaker;
// A non-empty JSON object also effectively introduces a JS syntax error.
return SniffForJSON(data);
// static
void CrossOriginReadBlocking::SanitizeBlockedResponse(
const scoped_refptr<network::ResourceResponse>& response) {
response->head.content_length = 0;
if (response->head.headers)
// static
void CrossOriginReadBlocking::LogAction(Action action) {
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Action", action);
// An interface to enable incremental content sniffing. These are instantiated
// for each each request; thus they can be stateful.
class CrossOriginReadBlocking::ResponseAnalyzer::ConfirmationSniffer {
virtual ~ConfirmationSniffer() = default;
// Called after data is read from the network. |sniffing_buffer| contains the
// entire response body delivered thus far. To support streaming,
// |new_data_offset| gives the offset into |sniffing_buffer| at which new data
// was appended since the last read.
virtual void OnDataAvailable(base::StringPiece sniffing_buffer,
size_t new_data_offset) = 0;
// Returns true if the return value of IsConfirmedContentType() might change
// with the addition of more data. Returns false if a final decision is
// available.
virtual bool WantsMoreData() const = 0;
// Returns true if the data has been confirmed to be of the CORB-protected
// content type that this sniffer is intended to detect.
virtual bool IsConfirmedContentType() const = 0;
// Helper for reporting the right UMA.
virtual bool IsParserBreakerSniffer() const = 0;
// A ConfirmationSniffer that wraps one of the sniffing functions from
// network::CrossOriginReadBlocking.
class CrossOriginReadBlocking::ResponseAnalyzer::SimpleConfirmationSniffer
: public CrossOriginReadBlocking::ResponseAnalyzer::ConfirmationSniffer {
// The function pointer type corresponding to one of the available sniffing
// functions from network::CrossOriginReadBlocking.
using SnifferFunction =
explicit SimpleConfirmationSniffer(SnifferFunction sniffer_function)
: sniffer_function_(sniffer_function) {}
~SimpleConfirmationSniffer() override = default;
void OnDataAvailable(base::StringPiece sniffing_buffer,
size_t new_data_offset) final {
DCHECK_LE(new_data_offset, sniffing_buffer.length());
if (new_data_offset == sniffing_buffer.length()) {
// No new data -- do nothing. This happens at end-of-stream.
// The sniffing functions don't support streaming, so with each new chunk of
// data, call the sniffer on the whole buffer.
last_sniff_result_ = (*sniffer_function_)(sniffing_buffer);
bool WantsMoreData() const final {
// kNo and kYes results are final, meaning that sniffing can stop once they
// occur. A kMaybe result corresponds to an indeterminate state, that could
// change to kYes or kNo with more data.
return last_sniff_result_ == SniffingResult::kMaybe;
bool IsConfirmedContentType() const final {
// Only confirm the mime type if an affirmative pattern (e.g. an HTML tag,
// if using the HTML sniffer) was detected.
// Note that if the stream ends (or net::kMaxBytesToSniff has been reached)
// and |last_sniff_result_| is kMaybe, the response is allowed to go
// through.
return last_sniff_result_ == SniffingResult::kYes;
bool IsParserBreakerSniffer() const override { return false; }
// The function that actually knows how to sniff for a content type.
SnifferFunction sniffer_function_;
// Result of sniffing the data available thus far.
SniffingResult last_sniff_result_ = SniffingResult::kMaybe;
// A ConfirmationSniffer for parser breakers (fetch-only resources). This logs
// to an UMA histogram whenever it is the reason for a response being blocked.
class CrossOriginReadBlocking::ResponseAnalyzer::FetchOnlyResourceSniffer
: public CrossOriginReadBlocking::ResponseAnalyzer::
SimpleConfirmationSniffer {
: SimpleConfirmationSniffer(
&network::CrossOriginReadBlocking::SniffForFetchOnlyResource) {}
bool IsParserBreakerSniffer() const override { return true; }
const net::URLRequest& request,
const ResourceResponse& response,
base::Optional<url::Origin> request_initiator_site_lock,
mojom::FetchRequestMode fetch_request_mode) {
// TODO(lukasza): Remove |initiator_compatibility_| field once the UMAs
// depending on it are expired (e.g. see
// SiteIsolation.XSD.NetworkService.InitiatorLockCompatibility).
initiator_compatibility_ = VerifyRequestInitiatorLock(
request_initiator_site_lock, request.initiator());
content_length_ = response.head.content_length;
http_response_code_ =
response.head.headers ? response.head.headers->response_code() : 0;
request_initiator_site_lock_ = request_initiator_site_lock;
should_block_based_on_headers_ =
ShouldBlockBasedOnHeaders(fetch_request_mode, request, response);
if (should_block_based_on_headers_ == kNeedToSniffMore)
CrossOriginReadBlocking::ResponseAnalyzer::~ResponseAnalyzer() = default;
mojom::FetchRequestMode fetch_request_mode,
const net::URLRequest& request,
const ResourceResponse& response) {
// The checks in this method are ordered to rule out blocking in most cases as
// quickly as possible. Checks that are likely to lead to returning false or
// that are inexpensive should be near the top.
url::Origin target_origin = url::Origin::Create(request.url());
// Check if |target_origin| seems to match the factory lock in
// |request_initiator_site_lock_|. If so, then treat this request as
// same-origin (even if |request.initiator()| might be cross-origin). See
// also
if (VerifyRequestInitiatorLock(request_initiator_site_lock_, target_origin) ==
InitiatorLockCompatibility::kCompatibleLock) {
return kAllow;
// Compute the |initiator| of the request, falling back to a unique origin if
// there was no initiator or if it was incompatible with the lock. Using a
// unique origin makes CORB treat the response as cross-origin and thus
// considers it eligible for blocking (based on content-type, sniffing, etc.).
url::Origin initiator = GetTrustworthyInitiator(
ShouldEnforceInitiatorLock() ? request_initiator_site_lock_
: base::nullopt,
// Don't block same-origin documents.
if (initiator.IsSameOriginWith(target_origin))
return kAllow;
// Only block documents from HTTP(S) schemes. Checking the scheme of
// |target_origin| ensures that we also protect content of blob: and
// filesystem: URLs if their nested origins have a HTTP(S) scheme.
if (!IsBlockableScheme(target_origin.GetURL()))
return kAllow;
// Allow requests from file:// URLs for now.
// TODO(creis): Limit this to when the allow_universal_access_from_file_urls
// preference is set. See
if (initiator.scheme() == url::kFileScheme)
return kAllow;
// Allow the response through if this is a CORS request and the response has
// valid CORS headers.
switch (fetch_request_mode) {
case mojom::FetchRequestMode::kNavigate:
case mojom::FetchRequestMode::kNoCors:
case mojom::FetchRequestMode::kSameOrigin:
case mojom::FetchRequestMode::kCors:
case mojom::FetchRequestMode::kCorsWithForcedPreflight:
std::string cors_header;
if (IsValidCorsHeaderSet(initiator, cors_header))
return kAllow;
// Requests from will consult's service worker
// first (if one has been registered). The service worker can handle requests
// initiated by even if they are cross-origin (e.g. requests
// for This is okay and should not be blocked by CORB,
// unless the initiator opted out of CORS / opted into receiving an opaque
// response. See also
if (response.head.was_fetched_via_service_worker) {
switch (response.head.response_type) {
case network::mojom::FetchResponseType::kBasic:
case network::mojom::FetchResponseType::kCors:
case network::mojom::FetchResponseType::kDefault:
case network::mojom::FetchResponseType::kError:
// Non-opaque responses shouldn't be blocked.
return kAllow;
case network::mojom::FetchResponseType::kOpaque:
case network::mojom::FetchResponseType::kOpaqueRedirect:
// Opaque responses are eligible for blocking. Continue on...
// We intend to block the response at this point. However, we will usually
// sniff the contents to confirm the MIME type, to avoid blocking incorrectly
// labeled JavaScript, JSONP, etc files.
// Note: if there is a nosniff header, it means we should honor the response
// mime type without trying to confirm it.
std::string nosniff_header;
bool has_nosniff_header =
base::LowerCaseEqualsASCII(nosniff_header, "nosniff");
// CORB should look directly at the Content-Type header if one has been
// received from the network. Ignoring |response.head.mime_type| helps avoid
// breaking legitimate websites (which might happen more often when blocking
// would be based on the mime type sniffed by MimeSniffingResourceHandler).
// TODO(nick): What if the mime type is omitted? Should that be treated the
// same as text/plain?
std::string mime_type;
if (response.head.headers)
// Canonicalize the MIME type. Note that even if it doesn't claim to be a
// blockable type (i.e., HTML, XML, JSON, or plain text), it may still fail
// the checks during the SniffForFetchOnlyResource() phase.
canonical_mime_type_ =
// CORS is currently implemented in the renderer process, so it's useful for
// CORB to filter failed "cors" mode fetches to avoid leaking the responses to
// the renderer when possible (e.g., depending on MIME type and sniffing).
// This will eventually be fixed with OOR-CORS.
// In the mean time, we can try to filter a few additional failed CORS
// fetches, treating the Cross-Origin-Resource-Policy (CORP) header as an
// opt-in to CORB. CORP headers are enforced elsewhere and normally only
// apply to "no-cors" mode fetches. If such a header happens to be on the
// response during other fetch modes, and if the same-origin and
// IsValidCorsHeaderSet checks above have failed (and thus the request will
// fail in the renderer), then we can let CORB filter the response without
// caring about MIME type or sniffing.
// To make CrossOriginResourcePolicy::Verify apply to all fetch modes in this
// case and not just "no-cors", we pass kNoCors as a hard-coded value. This
// does not affect the usual enforcement of CORP headers.
// TODO(lukasza): Once OOR-CORS launches (, this code
// block will no longer be necessary since all failed CORS requests will be
// blocked before reaching the renderer process (even without CORB's help).
// Of course this assumes that OOR-CORS will use trustworthy
// |request_initiator| (i.e. vetted against |request_initiator|site_lock|).
constexpr mojom::FetchRequestMode kOverreachingFetchMode =
if (CrossOriginResourcePolicy::kBlock ==
CrossOriginResourcePolicy::Verify(request, response,
request_initiator_site_lock_)) {
// Ignore mime types and/or sniffing and have CORB block all responses with
// COR*P* header.
return kBlock;
// If this is a partial response, sniffing is not possible, so allow the
// response if it's not a protected mime type.
std::string range_header;
response.head.headers->GetNormalizedHeader("content-range", &range_header);
bool has_range_header = !range_header.empty();
if (has_range_header) {
switch (canonical_mime_type_) {
case MimeType::kOthers:
case MimeType::kPlain: // See also
return kAllow;
case MimeType::kHtml:
case MimeType::kJson:
case MimeType::kXml:
return kBlock;
case MimeType::kMax:
return kBlock;
// Decide whether to block based on the MIME type.
switch (canonical_mime_type_) {
case MimeType::kHtml:
case MimeType::kXml:
case MimeType::kJson:
case MimeType::kPlain:
if (has_nosniff_header)
return kBlock;
return kNeedToSniffMore;
case MimeType::kOthers:
// Stylesheets shouldn't be sniffed for JSON parser breakers - see
if (base::LowerCaseEqualsASCII(response.head.mime_type, "text/css"))
return kAllow;
return kNeedToSniffMore;
case MimeType::kInvalidMimeType:
return kBlock;
return kBlock;
void CrossOriginReadBlocking::ResponseAnalyzer::CreateSniffers() {
// Create one or more |sniffers_| to confirm that the body is actually the
// MIME type advertised in the Content-Type header.
DCHECK_EQ(kNeedToSniffMore, should_block_based_on_headers_);
// When the MIME type is "text/plain", create sniffers for HTML, XML and
// JSON. If any of these sniffers match, the response will be blocked.
const bool use_all = canonical_mime_type() == MimeType::kPlain;
// HTML sniffer.
if (use_all || canonical_mime_type() == MimeType::kHtml) {
// XML sniffer.
if (use_all || canonical_mime_type() == MimeType::kXml) {
// JSON sniffer.
if (use_all || canonical_mime_type() == MimeType::kJson) {
// Parser-breaker sniffer.
// Because these prefixes are an XSSI-defeating mechanism, CORB considers
// them distinctive enough to be worth blocking no matter the Content-Type
// header. So this sniffer is created unconditionally.
// For MimeType::kOthers, this will be the only sniffer that's active.
void CrossOriginReadBlocking::ResponseAnalyzer::SniffResponseBody(
base::StringPiece data,
size_t new_data_offset) {
DCHECK_EQ(kNeedToSniffMore, should_block_based_on_headers_);
DCHECK_LE(bytes_read_for_sniffing_, static_cast<int>(data.size()));
bytes_read_for_sniffing_ = static_cast<int>(data.size());
DCHECK_LE(data.size(), static_cast<size_t>(net::kMaxBytesToSniff));
DCHECK_LE(new_data_offset, data.size());
bool has_new_data = (new_data_offset < data.size());
for (size_t i = 0; i < sniffers_.size();) {
if (has_new_data)
sniffers_[i]->OnDataAvailable(data, new_data_offset);
if (sniffers_[i]->WantsMoreData()) {
if (sniffers_[i]->IsConfirmedContentType()) {
if (sniffers_[i]->IsParserBreakerSniffer())
found_parser_breaker_ = true;
found_blockable_content_ = true;
} else {
// This response is CORB-exempt as far as this sniffer is concerned;
// remove it from the list.
sniffers_.erase(sniffers_.begin() + i);
bool CrossOriginReadBlocking::ResponseAnalyzer::ShouldAllow() const {
switch (should_block_based_on_headers_) {
case kAllow:
return true;
case kNeedToSniffMore:
return sniffers_.empty() && !found_blockable_content_;
case kBlock:
return false;
bool CrossOriginReadBlocking::ResponseAnalyzer::ShouldBlock() const {
switch (should_block_based_on_headers_) {
case kAllow:
return false;
case kNeedToSniffMore:
return sniffers_.empty() && found_blockable_content_;
case kBlock:
return true;
bool CrossOriginReadBlocking::ResponseAnalyzer::ShouldReportBlockedResponse()
const {
if (!ShouldBlock())
return false;
// Don't bother showing a warning message when blocking responses that are
// already empty.
if (content_length() == 0)
return false;
if (http_response_code() == 204)
return false;
// Don't bother showing a warning message when blocking responses that are
// associated with error responses (e.g. it is quite common to serve a
// text/html 404 error page for an <img> tag pointing to a wrong URL).
if (400 <= http_response_code() && http_response_code() <= 599)
return false;
return true;
void CrossOriginReadBlocking::ResponseAnalyzer::LogBytesReadForSniffing() {
if (bytes_read_for_sniffing_ >= 0) {
void CrossOriginReadBlocking::ResponseAnalyzer::LogAllowedResponse() {
// Note that if a response is allowed because of hitting EOF or
// kMaxBytesToSniff, then |sniffers_| are not emptied and consequently
// ShouldAllow doesn't start returning true. This means that we can't
// DCHECK(ShouldAllow()) or DCHECK(sniffers_.empty()) here - the decision to
// allow the response could have been made in the
// CrossSiteDocumentResourceHandler layer without CrossOriginReadBlocking
// realizing that it has hit EOF or kMaxBytesToSniff.
// Note that the response might be allowed even if ShouldBlock() returns true
// - for example to allow responses to requests initiated by content scripts.
// This means that we cannot DCHECK(!ShouldBlock()) here.
? network::CrossOriginReadBlocking::Action::kAllowedAfterSniffing
: network::CrossOriginReadBlocking::Action::kAllowedWithoutSniffing);
void CrossOriginReadBlocking::ResponseAnalyzer::LogBlockedResponse() {
? network::CrossOriginReadBlocking::Action::kBlockedAfterSniffing
: network::CrossOriginReadBlocking::Action::kBlockedWithoutSniffing);
content_length() >= 0);
if (content_length() >= 0) {
if (!ShouldReportBlockedResponse()) {
} else {
switch (initiator_compatibility_) {
case InitiatorLockCompatibility::kIncorrectLock:
case InitiatorLockCompatibility::kCompatibleLock:
// static
void CrossOriginReadBlocking::AddExceptionForPlugin(int process_id) {
std::set<int>& plugin_proxies = GetPluginProxyingProcesses();
// static
bool CrossOriginReadBlocking::ShouldAllowForPlugin(int process_id) {
std::set<int>& plugin_proxies = GetPluginProxyingProcesses();
return base::ContainsKey(plugin_proxies, process_id);
// static
void CrossOriginReadBlocking::RemoveExceptionForPlugin(int process_id) {
std::set<int>& plugin_proxies = GetPluginProxyingProcesses();
size_t number_of_elements_removed = plugin_proxies.erase(process_id);
DCHECK_EQ(1u, number_of_elements_removed);
} // namespace network