blob: e46328034033a00765c000186947fb5c545d1752 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/mixed_content_navigation_throttle.h"
#include <vector>
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/security_context/insecure_request_policy.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h"
#include "third_party/blink/public/mojom/frame/frame.mojom.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom.h"
#include "third_party/blink/public/mojom/security_context/insecure_request_policy.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
#include "url/url_util.h"
namespace content {
namespace {
bool IsSecureScheme(const std::string& scheme) {
return base::Contains(url::GetSecureSchemes(), scheme);
}
// Should return the same value as SecurityOrigin::isLocal and
// SchemeRegistry::shouldTreatURLSchemeAsCorsEnabled.
bool ShouldTreatURLSchemeAsCorsEnabled(const GURL& url) {
return base::Contains(url::GetCorsEnabledSchemes(), url.scheme());
}
// Should return the same value as the resource URL checks assigned to
// |isAllowed| made inside MixedContentChecker::isMixedContent.
bool IsUrlPotentiallySecure(const GURL& url) {
return network::IsUrlPotentiallyTrustworthy(url);
}
// This method should return the same results as
// SchemeRegistry::shouldTreatURLSchemeAsRestrictingMixedContent.
bool DoesOriginSchemeRestrictMixedContent(const url::Origin& origin) {
return origin.GetTupleOrPrecursorTupleIfOpaque().scheme() ==
url::kHttpsScheme;
}
void UpdateRendererOnMixedContentFound(NavigationRequest* navigation_request,
const GURL& mixed_content_url,
bool was_allowed,
bool for_redirect) {
// TODO(carlosk): the root node should never be considered as being/having
// mixed content for now. Once/if the browser should also check form submits
// for mixed content than this will be allowed to happen and this DCHECK
// should be updated.
DCHECK(navigation_request->GetParentFrameOrOuterDocument());
RenderFrameHostImpl* rfh =
navigation_request->frame_tree_node()->current_frame_host();
DCHECK(!navigation_request->GetRedirectChain().empty());
GURL url_before_redirects = navigation_request->GetRedirectChain()[0];
rfh->GetAssociatedLocalFrame()->MixedContentFound(
mixed_content_url, navigation_request->GetURL(),
navigation_request->request_context_type(), was_allowed,
url_before_redirects, for_redirect,
navigation_request->common_params().source_location.Clone());
}
} // namespace
// static
std::unique_ptr<NavigationThrottle>
MixedContentNavigationThrottle::CreateThrottleForNavigation(
NavigationHandle* navigation_handle) {
return std::make_unique<MixedContentNavigationThrottle>(navigation_handle);
}
MixedContentNavigationThrottle::MixedContentNavigationThrottle(
NavigationHandle* navigation_handle)
: NavigationThrottle(navigation_handle) {}
MixedContentNavigationThrottle::~MixedContentNavigationThrottle() {}
NavigationThrottle::ThrottleCheckResult
MixedContentNavigationThrottle::WillStartRequest() {
bool should_block = ShouldBlockNavigation(false);
return should_block ? CANCEL : PROCEED;
}
NavigationThrottle::ThrottleCheckResult
MixedContentNavigationThrottle::WillRedirectRequest() {
// Upon redirects the same checks are to be executed as for requests.
bool should_block = ShouldBlockNavigation(true);
if (!should_block) {
MaybeHandleCertificateError();
return PROCEED;
}
return CANCEL;
}
NavigationThrottle::ThrottleCheckResult
MixedContentNavigationThrottle::WillProcessResponse() {
MaybeHandleCertificateError();
return PROCEED;
}
const char* MixedContentNavigationThrottle::GetNameForLogging() {
return "MixedContentNavigationThrottle";
}
// Based off of MixedContentChecker::shouldBlockFetch.
bool MixedContentNavigationThrottle::ShouldBlockNavigation(bool for_redirect) {
NavigationRequest* request = NavigationRequest::From(navigation_handle());
FrameTreeNode* node = request->frame_tree_node();
// Find the parent frame where mixed content is characterized, if any.
RenderFrameHostImpl* mixed_content_frame =
InWhichFrameIsContentMixed(node, request->GetURL());
if (!mixed_content_frame) {
MaybeSendBlinkFeatureUsageReport();
return false;
}
// From this point on we know this is not a main frame navigation and that
// there is mixed content. Now let's decide if it's OK to proceed with it.
ReportBasicMixedContentFeatures(request->request_context_type(),
request->mixed_content_context_type());
// If we're in strict mode, we'll automagically fail everything, and
// intentionally skip the client/embedder checks in order to prevent degrading
// the site's security UI.
// TODO(crbug.com/1312424): Instead of checking node->IsInFencedFrameTree()
// set insecure_request_policy to block mixed content requests in a
// fenced frame tree and change InWhichFrameIsContentMixed to not cross
// the frame tree boundary.
bool block_all_mixed_content =
((mixed_content_frame->frame_tree_node()
->current_replication_state()
.insecure_request_policy &
blink::mojom::InsecureRequestPolicy::kBlockAllMixedContent) !=
blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone) ||
node->IsInFencedFrameTree();
const auto& prefs = mixed_content_frame->GetOrCreateWebPreferences();
bool strict_mode =
prefs.strict_mixed_content_checking || block_all_mixed_content;
blink::mojom::MixedContentContextType mixed_context_type =
request->mixed_content_context_type();
// Do not treat non-webby schemes as mixed content when loaded in subframes.
// Navigations to non-webby schemes cannot return data to the browser, so
// insecure content will not be run or displayed to the user as a result of
// loading a non-webby scheme. It is potentially dangerous to navigate to a
// non-webby scheme (e.g., the page could deliver a malicious payload to a
// vulnerable native application), but loading a non-webby scheme is no more
// dangerous in this respect than navigating the main frame to the non-webby
// scheme directly. See https://crbug.com/621131.
//
// TODO(https://crbug.com/1030307): decide whether CORS-enabled is really the
// right way to draw this distinction.
if (!ShouldTreatURLSchemeAsCorsEnabled(request->GetURL())) {
// Record non-webby mixed content to see if it is rare enough that it can be
// gated behind an enterprise policy. This excludes URLs that are considered
// potentially-secure such as blob: and filesystem:, which are special-cased
// in IsUrlPotentiallySecure() and cause an early-return because of the
// InWhichFrameIsContentMixed() check above.
UMA_HISTOGRAM_BOOLEAN("SSL.NonWebbyMixedContentLoaded", true);
return false;
}
// Cancel the prerendering page to prevent the problems that can be the
// logging UMA, UKM and calling DidChangeVisibleSecurityState() through this
// throttle.
if (mixed_content_frame->CancelPrerendering(
PrerenderHost::FinalStatus::kMixedContent)) {
return true;
}
bool allowed = false;
RenderFrameHostDelegate* frame_host_delegate =
node->current_frame_host()->delegate();
switch (mixed_context_type) {
case blink::mojom::MixedContentContextType::kOptionallyBlockable:
allowed = !strict_mode;
if (allowed) {
frame_host_delegate->PassiveInsecureContentFound(request->GetURL());
node->frame_tree()
->controller()
.ssl_manager()
->DidDisplayMixedContent();
}
break;
case blink::mojom::MixedContentContextType::kBlockable: {
// Note: from the renderer side implementation it seems like we don't need
// to care about reporting
// blink::UseCounter::BlockableMixedContentInSubframeBlocked because it is
// only triggered for sub-resources which are not checked for in the
// browser.
bool should_ask_delegate =
!strict_mode && (!prefs.strictly_block_blockable_mixed_content ||
prefs.allow_running_insecure_content);
allowed =
should_ask_delegate &&
frame_host_delegate->ShouldAllowRunningInsecureContent(
prefs.allow_running_insecure_content,
mixed_content_frame->GetLastCommittedOrigin(), request->GetURL());
if (allowed) {
const GURL& origin_url =
mixed_content_frame->GetLastCommittedOrigin().GetURL();
mixed_content_frame->OnDidRunInsecureContent(origin_url,
request->GetURL());
mixed_content_features_.insert(
blink::mojom::WebFeature::kMixedContentBlockableAllowed);
}
break;
}
case blink::mojom::MixedContentContextType::kShouldBeBlockable:
allowed = !strict_mode;
if (allowed)
node->frame_tree()
->controller()
.ssl_manager()
->DidDisplayMixedContent();
break;
case blink::mojom::MixedContentContextType::kNotMixedContent:
NOTREACHED();
break;
};
UpdateRendererOnMixedContentFound(request,
mixed_content_frame->GetLastCommittedURL(),
allowed, for_redirect);
MaybeSendBlinkFeatureUsageReport();
return !allowed;
}
// This method mirrors MixedContentChecker::inWhichFrameIsContentMixed but is
// implemented in a different form that seems more appropriate here.
RenderFrameHostImpl* MixedContentNavigationThrottle::InWhichFrameIsContentMixed(
FrameTreeNode* node,
const GURL& url) {
// Main frame navigations cannot be mixed content. But, fenced frame
// navigations should be considered as well because it can be mixed content.
// TODO(carlosk): except for form submissions which might be supported in the
// future.
if (!node->GetParentOrOuterDocument())
return nullptr;
// There's no mixed content if any of these are true:
// - The navigated URL is potentially secure.
// - Neither the root nor parent frames have secure origins.
// This next section diverges in how the Blink version is implemented but
// should get to the same results. Especially where isMixedContent calls
// exist, here they are partially fulfilled here and partially replaced by
// DoesOriginSchemeRestrictMixedContent.
RenderFrameHostImpl* mixed_content_frame = nullptr;
RenderFrameHostImpl* parent = node->GetParentOrOuterDocument();
RenderFrameHostImpl* root = parent->GetOutermostMainFrame();
if (!IsUrlPotentiallySecure(url)) {
// TODO(carlosk): we might need to check more than just the immediate parent
// and the root. See https://crbug.com/623486.
// Checks if the root and then the immediate parent frames' origins are
// secure.
if (DoesOriginSchemeRestrictMixedContent(root->GetLastCommittedOrigin()))
mixed_content_frame = root;
else if (DoesOriginSchemeRestrictMixedContent(
parent->GetLastCommittedOrigin())) {
mixed_content_frame = parent;
}
}
// Note: The code below should behave the same way as the two calls to
// measureStricterVersionOfIsMixedContent from inside
// MixedContentChecker::inWhichFrameIs.
if (mixed_content_frame) {
// We're currently only checking for mixed content in `https://*` contexts.
// What about other "secure" contexts the SchemeRegistry knows about? We'll
// use this method to measure the occurrence of non-webby mixed content to
// make sure we're not breaking the world without realizing it.
if (mixed_content_frame->GetLastCommittedOrigin().scheme() !=
url::kHttpsScheme) {
mixed_content_features_.insert(
blink::mojom::WebFeature::
kMixedContentInNonHTTPSFrameThatRestrictsMixedContent);
}
} else if (!network::IsUrlPotentiallyTrustworthy(url) &&
(IsSecureScheme(root->GetLastCommittedOrigin().scheme()) ||
IsSecureScheme(parent->GetLastCommittedOrigin().scheme()))) {
mixed_content_features_.insert(
blink::mojom::WebFeature::
kMixedContentInSecureFrameThatDoesNotRestrictMixedContent);
}
return mixed_content_frame;
}
void MixedContentNavigationThrottle::MaybeSendBlinkFeatureUsageReport() {
if (!mixed_content_features_.empty()) {
NavigationRequest* request = NavigationRequest::From(navigation_handle());
RenderFrameHostImpl* rfh = request->frame_tree_node()->current_frame_host();
rfh->GetAssociatedLocalFrame()->ReportBlinkFeatureUsage(
std::vector<blink::mojom::WebFeature>(mixed_content_features_.begin(),
mixed_content_features_.end()));
mixed_content_features_.clear();
}
}
// Based off of MixedContentChecker::count.
void MixedContentNavigationThrottle::ReportBasicMixedContentFeatures(
blink::mojom::RequestContextType request_context_type,
blink::mojom::MixedContentContextType mixed_content_context_type) {
mixed_content_features_.insert(
blink::mojom::WebFeature::kMixedContentPresent);
// Report any blockable content.
if (mixed_content_context_type ==
blink::mojom::MixedContentContextType::kBlockable) {
mixed_content_features_.insert(
blink::mojom::WebFeature::kMixedContentBlockable);
return;
}
// Note: as there's no mixed content checks for sub-resources on the browser
// side there should only be a subset of RequestContextType values that could
// ever be found here.
blink::mojom::WebFeature feature;
switch (request_context_type) {
case blink::mojom::RequestContextType::INTERNAL:
feature = blink::mojom::WebFeature::kMixedContentInternal;
break;
case blink::mojom::RequestContextType::PREFETCH:
feature = blink::mojom::WebFeature::kMixedContentPrefetch;
break;
case blink::mojom::RequestContextType::AUDIO:
case blink::mojom::RequestContextType::DOWNLOAD:
case blink::mojom::RequestContextType::FAVICON:
case blink::mojom::RequestContextType::IMAGE:
case blink::mojom::RequestContextType::PLUGIN:
case blink::mojom::RequestContextType::VIDEO:
default:
NOTREACHED() << "RequestContextType has value " << request_context_type
<< " and has MixedContentContextType of "
<< mixed_content_context_type;
return;
}
mixed_content_features_.insert(feature);
}
void MixedContentNavigationThrottle::MaybeHandleCertificateError() {
// The outermost main frame certificate errors are handled separately in
// SSLManager.
if (!navigation_handle()->GetParentFrameOrOuterDocument()) {
return;
}
// If there was no SSL info, then it was not an HTTPS resource load, and we
// can ignore it.
if (!navigation_handle()->GetSSLInfo()) {
return;
}
if (!net::IsCertStatusError(navigation_handle()->GetSSLInfo()->cert_status)) {
return;
}
NavigationRequest* request = NavigationRequest::From(navigation_handle());
RenderFrameHostImpl* rfh = request->frame_tree_node()->current_frame_host();
rfh->OnDidRunContentWithCertificateErrors();
}
// static
bool MixedContentNavigationThrottle::IsMixedContentForTesting(
const GURL& origin_url,
const GURL& url) {
const url::Origin origin = url::Origin::Create(origin_url);
return !IsUrlPotentiallySecure(url) &&
DoesOriginSchemeRestrictMixedContent(origin);
}
} // namespace content