blob: 907922701280b589bf11691342de0ec95cdec6a1 [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 <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/web_contents/web_contents_impl.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/site_isolation_policy.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"
using MimeType = network::CrossOriginReadBlocking::MimeType;
namespace content {
namespace {
// 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_;
};
} // namespace
// static
void CrossSiteDocumentResourceHandler::LogBlockedResponseOnUIThread(
ResourceRequestInfo::WebContentsGetter web_contents_getter,
bool needed_sniffing,
MimeType canonical_mime_type,
ResourceType resource_type,
int http_response_code,
int64_t content_length) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContents* web_contents = std::move(web_contents_getter).Run();
if (!web_contents)
return;
ukm::SourceId source_id = static_cast<WebContentsImpl*>(web_contents)
->GetUkmSourceIdForLastCommittedSource();
ukm::builders::SiteIsolation_XSD_Browser_Blocked(source_id)
.SetCanonicalMimeType(static_cast<int64_t>(canonical_mime_type))
.SetContentLengthWasZero(content_length == 0)
.SetContentResourceType(resource_type)
.SetHttpResponseCode(http_response_code)
.SetNeededSniffing(needed_sniffing)
.Record(ukm::UkmRecorder::Get());
}
void CrossSiteDocumentResourceHandler::LogBlockedResponse(
ResourceRequestInfoImpl* resource_request_info,
int http_response_code) {
DCHECK(resource_request_info);
DCHECK(analyzer_);
DCHECK_NE(network::CrossOriginReadBlocking::MimeType::kInvalidMimeType,
analyzer_->canonical_mime_type());
analyzer_->LogBlockedResponse();
ResourceType resource_type = resource_request_info->GetResourceType();
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked", resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
switch (analyzer_->canonical_mime_type()) {
case MimeType::kHtml:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.HTML",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case MimeType::kXml:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.XML",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case MimeType::kJson:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.JSON",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case MimeType::kPlain:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Plain",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
case MimeType::kOthers:
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Others",
resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
break;
default:
NOTREACHED();
}
if (analyzer_->found_parser_breaker()) {
UMA_HISTOGRAM_ENUMERATION(
"SiteIsolation.XSD.Browser.BlockedForParserBreaker", resource_type,
content::RESOURCE_TYPE_LAST_TYPE);
}
// 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(),
analyzer_->needs_sniffing(), analyzer_->canonical_mime_type(),
resource_type, http_response_code, analyzer_->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 ResumeForRedirect(const base::Optional<net::HttpRequestHeaders>&
modified_request_headers) override {
DCHECK(!modified_request_headers.has_value())
<< "Redirect with modified headers was not supported yet. "
"crbug.com/845683";
Resume();
}
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) {
has_response_started_ = true;
network::CrossOriginReadBlocking::LogAction(
network::CrossOriginReadBlocking::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 the sniffing needs).
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 confirmed_blockable = false;
ResourceRequestInfoImpl* info = GetRequestInfo();
if (!analyzer_->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 = true;
} 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.)
const size_t new_data_offset = local_buffer_bytes_read_;
local_buffer_bytes_read_ += bytes_read;
DCHECK_LE(local_buffer_bytes_read_, next_handler_buffer_size_);
const bool more_data_possible =
bytes_read != 0 && local_buffer_bytes_read_ < net::kMaxBytesToSniff &&
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 we have some new data, ask the |analyzer_| to sniff it.
analyzer_->SniffResponseBody(data, new_data_offset);
const bool confirmed_allowed = analyzer_->ShouldAllow();
confirmed_blockable = analyzer_->ShouldBlock();
DCHECK(!(confirmed_blockable && confirmed_allowed));
// If sniffing didn't yield a conclusive response, and we haven't read too
// many bytes yet or hit the end of the stream, buffer up some more data.
if (!(confirmed_blockable || confirmed_allowed) && more_data_possible) {
controller->Resume();
return;
}
}
// 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) {
// 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(info, analyzer_->http_response_code());
// Block the response and throw away the data. Report zero bytes read.
blocked_read_completed_ = true;
info->set_blocked_response_from_reaching_renderer(true);
if (analyzer_->ShouldReportBlockedResponse())
info->set_should_report_corb_blocking(true);
network::CrossOriginReadBlocking::SanitizeBlockedResponse(
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())
analyzer_->LogAllowedResponse();
next_handler_->OnResponseCompleted(status, std::move(controller));
}
}
bool CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders(
const network::ResourceResponse& response) {
// Give embedder a chance to skip document blocking for this response.
const char* initiator_scheme_exception =
GetContentClient()
->browser()
->GetInitatorSchemeBypassingDocumentBlocking();
// Delegate most decisions to CrossOriginReadBlocking::ResponseAnalyzer.
analyzer_ =
std::make_unique<network::CrossOriginReadBlocking::ResponseAnalyzer>(
*request(), response, initiator_scheme_exception);
if (analyzer_->ShouldAllow())
return false;
// 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: {
url::Origin target_origin = url::Origin::Create(request()->url());
if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites() &&
!ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedOrigin(
target_origin)) {
return false;
}
break;
}
case SiteIsolationPolicy::XSDB_DISABLED:
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;
// Don't block plugin requests.
// TODO(lukasza): Only disable CORB for plugins with universal access (see
// PepperURLLoaderHost::has_universal_access_), because only such plugins may
// have their own CORS-like mechanisms - e.g. crossdomain.xml in Flash). We
// should still enforce CORB for other kinds of plugins (i.e. ones without
// universal access).
if (info->GetResourceType() == RESOURCE_TYPE_PLUGIN_RESOURCE &&
is_nocors_plugin_request_) {
return false;
}
return true;
}
} // namespace content