blob: 475ead7f3af25546e0ff586aedfeb345980e1649 [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 <memory>
#include <string>
#include <utility>
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.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/common/site_isolation_policy.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/resource_context.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"
namespace content {
namespace {
void LogCrossSiteDocumentAction(
CrossSiteDocumentResourceHandler::Action action) {
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Action", action,
CrossSiteDocumentResourceHandler::Action::kCount);
}
} // namespace
// ResourceController used in OnWillRead in cases that sniffing will happen.
// When invoked, it runs the corresponding method on the ResourceHandler.
class CrossSiteDocumentResourceHandler::OnWillReadController
: public ResourceController {
public:
// Keeps track of the addresses of the ResourceLoader's buffer and size,
// which will be populated by the downstream ResourceHandler by the time that
// Resume() is called.
explicit OnWillReadController(
CrossSiteDocumentResourceHandler* document_handler,
scoped_refptr<net::IOBuffer>* buf,
int* buf_size)
: document_handler_(document_handler), buf_(buf), buf_size_(buf_size) {}
~OnWillReadController() override {}
// ResourceController implementation:
void Resume() override {
MarkAsUsed();
// Now that |buf_| has a buffer written into it by the downstream handler,
// set up sniffing in the CrossSiteDocumentResourceHandler.
document_handler_->ResumeOnWillRead(buf_, buf_size_);
}
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_;
// Address of the ResourceLoader's buffer, which will be populated by the
// downstream handler before Resume() is called.
scoped_refptr<net::IOBuffer>* buf_;
// Address of the size of |buf_|, similarly populated downstream.
int* buf_size_;
DISALLOW_COPY_AND_ASSIGN(OnWillReadController);
};
CrossSiteDocumentResourceHandler::CrossSiteDocumentResourceHandler(
std::unique_ptr<ResourceHandler> next_handler,
net::URLRequest* request,
bool is_nocors_plugin_request)
: LayeredResourceHandler(request, std::move(next_handler)),
is_nocors_plugin_request_(is_nocors_plugin_request) {}
CrossSiteDocumentResourceHandler::~CrossSiteDocumentResourceHandler() {}
void CrossSiteDocumentResourceHandler::OnResponseStarted(
ResourceResponse* response,
std::unique_ptr<ResourceController> controller) {
has_response_started_ = true;
LogCrossSiteDocumentAction(
CrossSiteDocumentResourceHandler::Action::kResponseStarted);
should_block_based_on_headers_ = ShouldBlockBasedOnHeaders(response);
next_handler_->OnResponseStarted(response, std::move(controller));
}
void CrossSiteDocumentResourceHandler::OnWillRead(
scoped_refptr<net::IOBuffer>* buf,
int* buf_size,
std::unique_ptr<ResourceController> controller) {
DCHECK(has_response_started_);
// 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 intended to block the response and haven't yet decided to allow it
// due to sniffing, we will read some of the data to a local buffer to sniff
// it. 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 an OnWillReadController to start the sniffing after the
// downstream handler has called Resume on it.
if (should_block_based_on_headers_ && !allow_based_on_sniffing_) {
HoldController(std::move(controller));
controller = std::make_unique<OnWillReadController>(this, buf, buf_size);
}
// Have the downstream handler(s) allocate the real buffer to use.
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 in cases that we intend to sniff the data, after
// downstream handler finishes its work from OnWillRead.
DCHECK(should_block_based_on_headers_);
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. This will be large, since the MIME sniffing
// handler is downstream. Technically we could use a smaller buffer if
// |needs_sniffing_| is false, but there's no need for the extra complexity.
DCHECK_GE(*buf_size, net::kMaxBytesToSniff);
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 probably 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 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.
if (should_block_based_on_headers_ && !allow_based_on_sniffing_) {
bool confirmed_blockable = false;
if (!needs_sniffing_) {
// TODO(creis): Also consider the MIME type confirmed if |bytes_read| is
// too small to do sniffing, or restructure to allow buffering enough.
// For now, responses with small initial reads may be allowed through.
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.)
UMA_HISTOGRAM_COUNTS("SiteIsolation.XSD.Browser.BytesReadForSniffing",
bytes_read);
DCHECK_LE(bytes_read, next_handler_buffer_size_);
base::StringPiece data(local_buffer_->data(), bytes_read);
// Confirm whether the data is HTML, XML, or JSON.
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 = CrossSiteDocumentClassifier::SniffForHTML(data) ||
CrossSiteDocumentClassifier::SniffForXML(data) ||
CrossSiteDocumentClassifier::SniffForJSON(data);
}
}
if (confirmed_blockable) {
// Block the response and throw away the data. Report zero bytes read.
bytes_read = 0;
blocked_read_completed_ = true;
// 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());
LogCrossSiteDocumentAction(
needs_sniffing_
? CrossSiteDocumentResourceHandler::Action::kBlockedAfterSniffing
: CrossSiteDocumentResourceHandler::Action::
kBlockedWithoutSniffing);
ResourceType resource_type = GetRequestInfo()->GetResourceType();
UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked",
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;
default:
NOTREACHED();
}
} else {
// Allow the response through instead and proceed with reading more.
// Copy sniffed data into the next handler's buffer before proceeding.
// Note that the size of the two buffers is the same (see OnWillRead).
DCHECK_LE(bytes_read, next_handler_buffer_size_);
memcpy(next_handler_buffer_->data(), local_buffer_->data(), bytes_read);
allow_based_on_sniffing_ = true;
}
// Clean up, whether we'll cancel or proceed from here.
local_buffer_ = nullptr;
next_handler_buffer_ = nullptr;
next_handler_buffer_size_ = 0;
}
next_handler_->OnReadCompleted(bytes_read, std::move(controller));
}
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 {
LogCrossSiteDocumentAction(
needs_sniffing_
? CrossSiteDocumentResourceHandler::Action::kAllowedAfterSniffing
: CrossSiteDocumentResourceHandler::Action::
kAllowedWithoutSniffing);
next_handler_->OnResponseCompleted(status, std::move(controller));
}
}
bool CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders(
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();
// 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(
url::Origin::Create(url))) {
return false;
}
break;
case SiteIsolationPolicy::XSDB_DISABLED:
return false;
}
// Look up MIME type. If it doesn't claim to be a blockable type (i.e., HTML,
// XML, JSON, or plain text), don't block it.
canonical_mime_type_ = CrossSiteDocumentClassifier::GetCanonicalMimeType(
response->head.mime_type);
if (canonical_mime_type_ == CROSS_SITE_DOCUMENT_MIME_TYPE_OTHERS)
return false;
// 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-site documents.
if (CrossSiteDocumentClassifier::IsSameSite(initiator, url))
return false;
// Only block documents from HTTP(S) schemes.
if (!CrossSiteDocumentClassifier::IsBlockableScheme(url))
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, url,
cors_header)) {
return false;
}
// 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: only sniff if there isn't a nosniff header, and if it is not a range
// request. Range requests would let an attacker bypass blocking by
// requesting a range that fails to sniff as a protected type.
std::string nosniff_header;
response->head.headers->GetNormalizedHeader("x-content-type-options",
&nosniff_header);
std::string range_header;
response->head.headers->GetNormalizedHeader("content-range", &range_header);
needs_sniffing_ = !base::LowerCaseEqualsASCII(nosniff_header, "nosniff") &&
range_header.empty();
return true;
}
} // namespace content