blob: f5bd0bee3b83970f9f90da106ee979ced9275965 [file] [log] [blame]
// 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 <string.h>
#include <algorithm>
#include <string>
#include <unordered_set>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/loader/detachable_resource_handler.h"
#include "content/browser/loader/resource_request_info_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/site_isolation_policy.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/resource_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "net/base/io_buffer.h"
#include "net/base/mime_sniffer.h"
#include "net/url_request/url_request.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
namespace content {
namespace {
void LogCrossSiteDocumentAction(
CrossSiteDocumentResourceHandler::Action action) {
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Action", action,
CrossSiteDocumentResourceHandler::Action::kCount);
}
// An IOBuffer to enable writing into a existing IOBuffer at a given offset.
class LocalIoBufferWithOffset : public net::WrappedIOBuffer {
public:
LocalIoBufferWithOffset(net::IOBuffer* buf, int offset)
: net::WrappedIOBuffer(buf->data() + offset), buf_(buf) {}
private:
~LocalIoBufferWithOffset() override {}
scoped_refptr<net::IOBuffer> buf_;
};
// Helper for the text/plain case.
CrossSiteDocumentClassifier::Result SniffForHtmlXmlOrJson(
base::StringPiece data) {
DCHECK_LT(CrossSiteDocumentClassifier::kNo,
CrossSiteDocumentClassifier::kMaybe);
auto result = CrossSiteDocumentClassifier::SniffForHTML(data);
if (result != CrossSiteDocumentClassifier::kYes)
result = std::max(CrossSiteDocumentClassifier::SniffForXML(data), result);
if (result != CrossSiteDocumentClassifier::kYes)
result = std::max(CrossSiteDocumentClassifier::SniffForJSON(data), result);
return result;
}
// Headers from
// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name.
//
// Note that XSDB 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.
const char* const kCorsSafelistedHeaders[] = {
"cache-control", "content-language", "content-type",
"expires", "last-modified", "pragma",
};
// Removes headers that should be blocked in cross-origin case.
// See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name.
void SanitizeResponseHeaders(
const scoped_refptr<net::HttpResponseHeaders>& headers) {
DCHECK(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)) {
continue;
}
// Remove all other headers (but note the final exclusion below).
names_of_headers_to_remove.insert(base::ToLowerASCII(name));
}
// Exclude from removals headers from
// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name.
for (const char* header : kCorsSafelistedHeaders)
names_of_headers_to_remove.erase(header);
headers->RemoveHeaders(names_of_headers_to_remove);
}
// Sanitizes/strips metadata of a response we decided to block.
void SanitizeResourceResponse(
const scoped_refptr<network::ResourceResponse>& response) {
DCHECK(response);
response->head.content_length = 0;
if (response->head.headers)
SanitizeResponseHeaders(response->head.headers);
}
} // namespace
// static
void CrossSiteDocumentResourceHandler::LogBlockedResponseOnUIThread(
ResourceRequestInfo::WebContentsGetter web_contents_getter,
bool needed_sniffing,
CrossSiteDocumentMimeType canonical_mime_type,
ResourceType resource_type,
int http_response_code,
int64_t content_length) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContents* web_contents = web_contents_getter.Run();
if (!web_contents)
return;
ukm::UkmRecorder* recorder = ukm::UkmRecorder::Get();
ukm::SourceId source_id = ukm::UkmRecorder::GetNewSourceID();
recorder->UpdateSourceURL(source_id, web_contents->GetLastCommittedURL());
ukm::builders::SiteIsolation_XSD_Browser_Blocked(source_id)
.SetCanonicalMimeType(canonical_mime_type)
.SetContentLengthWasZero(content_length == 0)
.SetContentResourceType(resource_type)
.SetHttpResponseCode(http_response_code)
.SetNeededSniffing(needed_sniffing)
.Record(recorder);
}
// static
void CrossSiteDocumentResourceHandler::LogBlockedResponse(
ResourceRequestInfoImpl* resource_request_info,
bool needed_sniffing,
bool found_parser_breaker,
CrossSiteDocumentMimeType canonical_mime_type,
int http_response_code,
int64_t content_length) {
LogCrossSiteDocumentAction(
needed_sniffing
? CrossSiteDocumentResourceHandler::Action::kBlockedAfterSniffing
: CrossSiteDocumentResourceHandler::Action::kBlockedWithoutSniffing);
UMA_HISTOGRAM_BOOLEAN(
"SiteIsolation.XSD.Browser.Blocked.ContentLength.WasAvailable",
content_length >= 0);
if (content_length >= 0) {
UMA_HISTOGRAM_COUNTS_10000(
"SiteIsolation.XSD.Browser.Blocked.ContentLength.ValueIfAvailable",
content_length);
}
ResourceType resource_type = resource_request_info->GetResourceType();
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked", resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
if (found_parser_breaker) {
UMA_HISTOGRAM_ENUMERATION(
"SiteIsolation.XSD.Browser.BlockedForParserBreaker", resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
}
switch (canonical_mime_type) {
case CROSS_SITE_DOCUMENT_MIME_TYPE_HTML:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.HTML",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case CROSS_SITE_DOCUMENT_MIME_TYPE_XML:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.XML",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case CROSS_SITE_DOCUMENT_MIME_TYPE_JSON:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.JSON",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Plain",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Others",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
default:
NOTREACHED();
}
// The last committed URL is only available on the UI thread - we need to hop
// onto the UI thread to log an UKM event. Note that this is racey - by the
// time the posted task runs, the WebContents could have been closed and/or
// navigated to another URL. This is understood and acceptable - this should
// be rare enough to not matter for the collected UKM data.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(
&CrossSiteDocumentResourceHandler::LogBlockedResponseOnUIThread,
resource_request_info->GetWebContentsGetterForRequest(),
needed_sniffing, canonical_mime_type, resource_type,
http_response_code, content_length));
}
// ResourceController that runs a closure on Resume(), and forwards failures
// back to CrossSiteDocumentHandler. The closure can optionally be run as
// a PostTask.
class CrossSiteDocumentResourceHandler::Controller : public ResourceController {
public:
explicit Controller(CrossSiteDocumentResourceHandler* document_handler,
bool post_task,
base::OnceClosure resume_callback)
: document_handler_(document_handler),
resume_callback_(std::move(resume_callback)),
post_task_(post_task) {}
~Controller() override {}
// ResourceController implementation:
void Resume() override {
MarkAsUsed();
if (post_task_) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, std::move(resume_callback_));
} else {
std::move(resume_callback_).Run();
}
}
void Cancel() override {
MarkAsUsed();
document_handler_->Cancel();
}
void CancelWithError(int error_code) override {
MarkAsUsed();
document_handler_->CancelWithError(error_code);
}
private:
void MarkAsUsed() {
#if DCHECK_IS_ON()
DCHECK(!used_);
used_ = true;
#endif
}
#if DCHECK_IS_ON()
bool used_ = false;
#endif
CrossSiteDocumentResourceHandler* document_handler_;
// Runs on Resume().
base::OnceClosure resume_callback_;
bool post_task_;
DISALLOW_COPY_AND_ASSIGN(Controller);
};
CrossSiteDocumentResourceHandler::CrossSiteDocumentResourceHandler(
std::unique_ptr<ResourceHandler> next_handler,
net::URLRequest* request,
bool is_nocors_plugin_request)
: LayeredResourceHandler(request, std::move(next_handler)),
weak_next_handler_(next_handler_.get()),
is_nocors_plugin_request_(is_nocors_plugin_request),
weak_this_(this) {}
CrossSiteDocumentResourceHandler::~CrossSiteDocumentResourceHandler() {}
void CrossSiteDocumentResourceHandler::OnResponseStarted(
network::ResourceResponse* response,
std::unique_ptr<ResourceController> controller) {
content_length_ = response->head.content_length;
has_response_started_ = true;
http_response_code_ =
response->head.headers ? response->head.headers->response_code() : 0;
LogCrossSiteDocumentAction(
CrossSiteDocumentResourceHandler::Action::kResponseStarted);
should_block_based_on_headers_ = ShouldBlockBasedOnHeaders(response);
// If blocking is possible, postpone forwarding |response| to the
// |next_handler_|, until we have made the allow-vs-block decision
// (which might need more time depending on |needs_sniffing_|).
if (should_block_based_on_headers_) {
pending_response_start_ = response;
controller->Resume();
} else {
next_handler_->OnResponseStarted(response, std::move(controller));
}
}
void CrossSiteDocumentResourceHandler::OnWillRead(
scoped_refptr<net::IOBuffer>* buf,
int* buf_size,
std::unique_ptr<ResourceController> controller) {
// For allowed responses, the data is directly streamed to the next handler.
// Note that OnWillRead may be called before OnResponseStarted (because the
// MimeSniffingResourceHandler upstream changes the order of the calls) - this
// means that |has_response_started_| has to be explicitly checked below.
if (has_response_started_ &&
(!should_block_based_on_headers_ || allow_based_on_sniffing_)) {
DCHECK(!local_buffer_);
next_handler_->OnWillRead(buf, buf_size, std::move(controller));
return;
}
// If |local_buffer_| exists, continue buffering data into the end of it.
if (local_buffer_) {
// Check that we still have room for more local bufferring.
DCHECK_GT(next_handler_buffer_size_, local_buffer_bytes_read_);
*buf = new LocalIoBufferWithOffset(local_buffer_.get(),
local_buffer_bytes_read_);
*buf_size = next_handler_buffer_size_ - local_buffer_bytes_read_;
controller->Resume();
return;
}
// On the next read attempt after the response was blocked, either cancel the
// rest of the request or allow it to proceed in a detached state.
if (blocked_read_completed_) {
DCHECK(should_block_based_on_headers_);
DCHECK(!allow_based_on_sniffing_);
const ResourceRequestInfoImpl* info = GetRequestInfo();
if (info && info->detachable_handler()) {
// Ensure that prefetch, etc, continue to cache the response, without
// sending it to the renderer.
info->detachable_handler()->Detach();
} else {
// If it's not detachable, cancel the rest of the request.
controller->Cancel();
}
return;
}
// If we haven't yet decided to allow or block the response, we should read
// the data into a local buffer 1) to temporarily prevent the data from
// reaching the renderer and 2) to potentially sniff the data to confirm the
// content type.
//
// Since the downstream handler may defer during the OnWillRead call below,
// the values of |buf| and |buf_size| may not be available right away.
// Instead, create a Controller that will start the sniffing after the
// downstream handler has called Resume on it.
HoldController(std::move(controller));
controller = std::make_unique<Controller>(
this, false /* post_task */,
base::BindOnce(&CrossSiteDocumentResourceHandler::ResumeOnWillRead,
weak_this_.GetWeakPtr(), buf, buf_size));
next_handler_->OnWillRead(buf, buf_size, std::move(controller));
}
void CrossSiteDocumentResourceHandler::ResumeOnWillRead(
scoped_refptr<net::IOBuffer>* buf,
int* buf_size) {
// We should only get here if we haven't yet made a block-vs-allow decision
// (we get here after downstream handler finishes its work from OnWillRead).
DCHECK(!allow_based_on_sniffing_);
DCHECK(!blocked_read_completed_);
// For most blocked responses, we need to sniff the data to confirm it looks
// like the claimed MIME type (to avoid blocking mislabeled JavaScript,
// JSONP, etc). Read this data into a separate buffer (not shared with the
// renderer), which we will only copy over if we decide to allow it through.
// This is only done when we suspect the response should be blocked.
//
// Make it as big as the downstream handler's buffer to make it easy to copy
// over in one operation.
DCHECK_GE(*buf_size, net::kMaxBytesToSniff * 2);
local_buffer_ =
base::MakeRefCounted<net::IOBuffer>(static_cast<size_t>(*buf_size));
// Store the next handler's buffer but don't read into it while sniffing,
// since we possibly won't want to send the data to the renderer process.
next_handler_buffer_ = *buf;
next_handler_buffer_size_ = *buf_size;
*buf = local_buffer_;
Resume();
}
void CrossSiteDocumentResourceHandler::OnReadCompleted(
int bytes_read,
std::unique_ptr<ResourceController> controller) {
DCHECK(has_response_started_);
DCHECK(!blocked_read_completed_);
if (!should_block_based_on_headers_) {
// CrossSiteDocumentResourceHandler always intercepts the buffer allocated
// by the first call to |next_handler_|'s OnWillRead and passes the
// |local_buffer_| upstream. If we decide not to block based on headers,
// then the data needs to be passed into the |next_handler_|.
if (local_buffer_) {
DCHECK_EQ(0, local_buffer_bytes_read_);
local_buffer_bytes_read_ = bytes_read;
StopLocalBuffering(true /* = copy_data_to_next_handler */);
}
next_handler_->OnReadCompleted(bytes_read, std::move(controller));
return;
}
if (allow_based_on_sniffing_) {
// If CrossSiteDocumentResourceHandler decided to allow the response based
// on sniffing, then StopLocalBuffering was already called below by the
// previous execution of CrossSiteDocumentResourceHandler::OnReadCompleted.
// From there onward, we just need to foward all the calls to the
// |next_handler_|.
DCHECK(!local_buffer_);
next_handler_->OnReadCompleted(bytes_read, std::move(controller));
return;
}
// If we intended to block the response and haven't sniffed yet, try to
// confirm that we should block it. If sniffing is needed, look at the local
// buffer and either report that zero bytes were read (to indicate the
// response is empty and complete), or copy the sniffed data to the next
// handler's buffer and resume the response without blocking.
bool found_parser_breaker = false;
auto confirmed_blockable = CrossSiteDocumentClassifier::kNo;
if (!needs_sniffing_) {
// If sniffing is impossible (e.g., because this is a range request), or
// if sniffing is disabled due to a nosniff header AND the server returned
// a protected mime type, then we have enough information to block
// immediately.
confirmed_blockable = CrossSiteDocumentClassifier::kYes;
} else if (bytes_read == 0) {
// We haven't blocked the response yet (because previous reads yielded a
// kMaybe result), and there is no more data. Allow the response.
confirmed_blockable = CrossSiteDocumentClassifier::kNo;
} else {
// Sniff the data to see if it likely matches the MIME type that caused us
// to decide to block it. If it doesn't match, it may be JavaScript,
// JSONP, or another allowable data type and we should let it through.
// Record how many bytes were read to see how often it's too small. (This
// will typically be under 100,000.)
local_buffer_bytes_read_ += bytes_read;
DCHECK_LE(local_buffer_bytes_read_, next_handler_buffer_size_);
// To ensure determinism with respect to network packet ordering and
// sizing, never examine more than kMaxBytesToSniff bytes, even if more
// are available.
size_t bytes_to_sniff =
std::min(local_buffer_bytes_read_, net::kMaxBytesToSniff);
base::StringPiece data(local_buffer_->data(), bytes_to_sniff);
// If the server returned a protected mime type, sniff the response to
// confirm it.
if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_HTML) {
confirmed_blockable = CrossSiteDocumentClassifier::SniffForHTML(data);
} else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_XML) {
confirmed_blockable = CrossSiteDocumentClassifier::SniffForXML(data);
} else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_JSON) {
confirmed_blockable = CrossSiteDocumentClassifier::SniffForJSON(data);
} else if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_PLAIN) {
// For responses labeled as plain text, only block them if the data
// sniffs as one of the formats we would block in the first place.
confirmed_blockable = SniffForHtmlXmlOrJson(data);
}
// Additionally, on all mime types (including _OTHERS), look for
// Javascript parser breakers. These are affirmative patterns that
// indicate this resource should only be consumed by XHR/fetch (and we've
// already verified that this response isn't a permissable cross-origin
// XHR/fetch).
if (confirmed_blockable != CrossSiteDocumentClassifier::kYes &&
non_stylesheet_mime_type_ /* see https://crbug.com/809259 */) {
auto result =
CrossSiteDocumentClassifier::SniffForFetchOnlyResource(data);
found_parser_breaker = (result == CrossSiteDocumentClassifier::kYes);
confirmed_blockable = std::max(confirmed_blockable, result);
}
// If sniffing didn't yield a conclusive response, and we haven't read too
// many bytes yet, buffer up some more data.
if (confirmed_blockable == CrossSiteDocumentClassifier::kMaybe &&
local_buffer_bytes_read_ < net::kMaxBytesToSniff &&
local_buffer_bytes_read_ < next_handler_buffer_size_) {
controller->Resume();
return;
}
}
if (needs_sniffing_) {
UMA_HISTOGRAM_COUNTS("SiteIsolation.XSD.Browser.BytesReadForSniffing",
local_buffer_bytes_read_);
}
// At this point we have already made a block-vs-allow decision and we know
// that we can wake the |next_handler_| and let it catch-up with our
// processing of the response. The first step will always be calling
// |next_handler_->OnResponseStarted(...)|, but we need to figure out what
// other steps need to happen, before we can resume the real response
// upstream. These steps will be gathered into |controller|.
// The last step will always be calling
// CrossSiteDocumentResourceHandler::Resume.
HoldController(std::move(controller));
controller = std::make_unique<Controller>(
this, false /* post_task */,
base::BindOnce(&CrossSiteDocumentResourceHandler::Resume,
weak_this_.GetWeakPtr()));
if (confirmed_blockable == CrossSiteDocumentClassifier::kYes) {
// Log the blocking event. Inline the Serialize call to avoid it when
// tracing is disabled.
TRACE_EVENT2("navigation",
"CrossSiteDocumentResourceHandler::ShouldBlockResponse",
"initiator",
request()->initiator().has_value()
? request()->initiator().value().Serialize()
: "null",
"url", request()->url().spec());
LogBlockedResponse(GetRequestInfo(), needs_sniffing_, found_parser_breaker,
canonical_mime_type_, http_response_code_,
content_length_);
// Block the response and throw away the data. Report zero bytes read.
blocked_read_completed_ = true;
ResourceRequestInfoImpl* info = GetRequestInfo();
info->set_blocked_cross_site_document(true);
SanitizeResourceResponse(pending_response_start_);
// Pass an empty/blocked body onto the next handler. size of the two
// buffers is the same (see OnWillRead). After the next statement,
// |controller| will store a sequence of steps like this:
// - next_handler_->OnReadCompleted(bytes_read = 0)
// - ... steps from the old |controller| (typically this->Resume()) ...
controller = std::make_unique<Controller>(
this, true /* post_task */,
base::BindOnce(&ResourceHandler::OnReadCompleted,
weak_next_handler_.GetWeakPtr(), 0 /* bytes_read */,
std::move(controller)));
StopLocalBuffering(false /* = copy_data_to_next_handler*/);
} else {
// Choose not block this response.
allow_based_on_sniffing_ = true;
if (bytes_read == 0 && local_buffer_bytes_read_ != 0) {
// |bytes_read == 0| indicates the end-of-stream. In this case, we need
// to synthesize an additional OnWillRead() and OnReadCompleted(0) on
// |next_handler_|, so that |next_handler_| sees both the full response
// and the end-of-stream marker. After the next statement, |controller|
// will store a sequence of steps like this:
// - next_handler_->OnWillRead(...)
// - next_handler_->OnReadCompleted(bytes_read = 0)
// - ... steps from the old |controller| (typically this->Resume()) ...
//
// Note that if |weak_next_handler_| is alive, then |this| should also be
// alive and therefore it is safe to dereference |&next_handler_buffer_|
// and |&next_handler_buffer_size_|.
controller = std::make_unique<Controller>(
this, false /* post_task */,
base::BindOnce(
&ResourceHandler::OnWillRead, weak_next_handler_.GetWeakPtr(),
&next_handler_buffer_, &next_handler_buffer_size_,
std::make_unique<Controller>(
this, true /* post_task */,
base::BindOnce(&ResourceHandler::OnReadCompleted,
weak_next_handler_.GetWeakPtr(),
0 /* bytes_read */, std::move(controller)))));
}
// Pass the contents of |local_buffer_| onto the next handler. Afterwards,
// |controller| will store a sequence of steps like this:
// - next_handler_->OnReadCompleted(local_buffer_bytes_read_)
// - ... steps from the old |controller| ...
controller = std::make_unique<Controller>(
this, true /* post_task */,
base::BindOnce(&ResourceHandler::OnReadCompleted,
weak_next_handler_.GetWeakPtr(),
local_buffer_bytes_read_, std::move(controller)));
StopLocalBuffering(true /* = copy_data_to_next_handler*/);
}
// In both the blocked and allowed cases, we need to resume by notifying the
// downstream handler about the response start.
DCHECK(pending_response_start_);
next_handler_->OnResponseStarted(pending_response_start_.get(),
std::move(controller));
pending_response_start_ = nullptr;
}
void CrossSiteDocumentResourceHandler::StopLocalBuffering(
bool copy_data_to_next_handler) {
DCHECK(has_response_started_);
DCHECK(!should_block_based_on_headers_ || allow_based_on_sniffing_ ||
blocked_read_completed_);
DCHECK(local_buffer_);
DCHECK(next_handler_buffer_);
if (copy_data_to_next_handler) {
// Pass the contents of |local_buffer_| onto the next handler. Note that the
// size of the two buffers is the same (see OnWillRead).
DCHECK_LE(local_buffer_bytes_read_, next_handler_buffer_size_);
memcpy(next_handler_buffer_->data(), local_buffer_->data(),
local_buffer_bytes_read_);
}
local_buffer_ = nullptr;
local_buffer_bytes_read_ = 0;
next_handler_buffer_ = nullptr;
next_handler_buffer_size_ = 0;
}
void CrossSiteDocumentResourceHandler::OnResponseCompleted(
const net::URLRequestStatus& status,
std::unique_ptr<ResourceController> controller) {
if (blocked_read_completed_) {
// Report blocked responses as successful, rather than the cancellation
// from OnWillRead.
next_handler_->OnResponseCompleted(net::URLRequestStatus(),
std::move(controller));
} else {
// Only report XSDB status for successful (i.e. non-aborted,
// non-errored-out) requests.
if (status.is_success()) {
LogCrossSiteDocumentAction(
needs_sniffing_
? CrossSiteDocumentResourceHandler::Action::kAllowedAfterSniffing
: CrossSiteDocumentResourceHandler::Action::
kAllowedWithoutSniffing);
}
next_handler_->OnResponseCompleted(status, std::move(controller));
}
}
bool CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders(
network::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.
const GURL& url = request()->url();
url::Origin target_origin = url::Origin::Create(url);
// Check if the response's site needs to have its documents protected. By
// default, this will usually return false.
// TODO(creis): This check can go away once the logic here is made fully
// backward compatible and we can enforce it always, regardless of Site
// Isolation policy.
switch (SiteIsolationPolicy::IsCrossSiteDocumentBlockingEnabled()) {
case SiteIsolationPolicy::XSDB_ENABLED_UNCONDITIONALLY:
break;
case SiteIsolationPolicy::XSDB_ENABLED_IF_ISOLATED:
if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites() &&
!ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedOrigin(
target_origin)) {
return false;
}
break;
case SiteIsolationPolicy::XSDB_DISABLED:
return false;
}
// 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? https://crbug.com/795971
std::string mime_type;
if (response->head.headers)
response->head.headers->GetMimeType(&mime_type);
// 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_ =
CrossSiteDocumentClassifier::GetCanonicalMimeType(mime_type);
// Treat a missing initiator as an empty origin to be safe, though we don't
// expect this to happen. Unfortunately, this requires a copy.
url::Origin initiator;
if (request()->initiator().has_value())
initiator = request()->initiator().value();
// Don't block same-origin documents.
if (initiator.IsSameOriginWith(target_origin))
return false;
// 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 (!CrossSiteDocumentClassifier::IsBlockableScheme(target_origin.GetURL()))
return false;
// Allow requests from file:// URLs for now.
// TODO(creis): Limit this to when the allow_universal_access_from_file_urls
// preference is set. See https://crbug.com/789781.
if (initiator.scheme() == url::kFileScheme)
return false;
// Only block if this is a request made from a renderer process.
const ResourceRequestInfoImpl* info = GetRequestInfo();
if (!info || info->GetChildID() == -1)
return false;
// Give embedder a chance to skip document blocking for this response.
if (GetContentClient()->browser()->ShouldBypassDocumentBlocking(
initiator, url, info->GetResourceType())) {
return false;
}
// Allow the response through if it has valid CORS headers.
std::string cors_header;
response->head.headers->GetNormalizedHeader("access-control-allow-origin",
&cors_header);
if (CrossSiteDocumentClassifier::IsValidCorsHeaderSet(initiator,
cors_header)) {
return false;
}
// Requests from foo.example.com will consult foo.example.com's service worker
// first (if one has been registered). The service worker can handle requests
// initiated by foo.example.com even if they are cross-origin (e.g. requests
// for bar.example.com). This is okay and should not be blocked by XSDB,
// unless the initiator opted out of CORS / opted into receiving an opaque
// response. See also https://crbug.com/803672.
if (response->head.was_fetched_via_service_worker) {
switch (response->head.response_type_via_service_worker) {
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 false;
case network::mojom::FetchResponseType::kOpaque:
case network::mojom::FetchResponseType::kOpaqueRedirect:
// Opaque responses are eligible for blocking. Continue on...
break;
}
}
// Don't block plugin requests with universal access (e.g., Flash). Such
// requests are made without CORS, and thus dont have an Origin request
// header. Other plugin requests (e.g., NaCl) are made using CORS and have an
// Origin request header. If they fail the CORS check above, they should be
// blocked.
if (info->GetResourceType() == RESOURCE_TYPE_PLUGIN_RESOURCE &&
is_nocors_plugin_request_) {
return false;
}
// 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;
response->head.headers->GetNormalizedHeader("x-content-type-options",
&nosniff_header);
bool has_nosniff_header =
base::LowerCaseEqualsASCII(nosniff_header, "nosniff");
// If this is an HTTP range request, sniffing isn't possible.
std::string range_header;
response->head.headers->GetNormalizedHeader("content-range", &range_header);
bool has_range_header = !range_header.empty();
// If this is a partial response, sniffing is not possible, so allow the
// response if it's not a protected mime type.
if (has_range_header &&
canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS) {
return false;
}
// We need to sniff unprotected mime types (e.g. for parser breakers), and
// unless the nosniff header is set, we also need to sniff protected mime
// types to verify that they're not mislabeled.
needs_sniffing_ =
(canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS) ||
!(has_range_header || has_nosniff_header);
// Stylesheets shouldn't be sniffed for JSON parser breakers - see
// https://crbug.com/809259.
non_stylesheet_mime_type_ =
!response->head.mime_type.empty() &&
!base::LowerCaseEqualsASCII(response->head.mime_type, "text/css");
return true;
}
// static
std::vector<std::string>
CrossSiteDocumentResourceHandler::GetCorsSafelistedHeadersForTesting() {
return std::vector<std::string>(
kCorsSafelistedHeaders,
kCorsSafelistedHeaders + arraysize(kCorsSafelistedHeaders));
}
} // namespace content