blob: 903dfb39181b84106df8105ae688251709fab870 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "core/loader/MixedContentChecker.h"
#include "core/dom/Document.h"
#include "core/frame/Frame.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/inspector/ConsoleMessage.h"
#include "core/loader/DocumentLoader.h"
#include "core/loader/FrameLoader.h"
#include "core/loader/FrameLoaderClient.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/network/NetworkUtils.h"
#include "platform/weborigin/SchemeRegistry.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "public/platform/WebAddressSpace.h"
#include "public/platform/WebInsecureRequestPolicy.h"
#include "wtf/text/StringBuilder.h"
namespace blink {
namespace {
// When a frame is local, use its full URL to represent the main resource. When
// the frame is remote, the full URL isn't accessible, so use the origin. This
// function is used, for example, to determine the URL to show in console
// messages about mixed content.
KURL mainResourceUrlForFrame(Frame* frame) {
if (frame->isRemoteFrame())
return KURL(KURL(),
frame->securityContext()->getSecurityOrigin()->toString());
return toLocalFrame(frame)->document()->url();
}
} // namespace
static void measureStricterVersionOfIsMixedContent(Frame* frame,
const KURL& url) {
// 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 occurance of non-webby mixed content to make
// sure we're not breaking the world without realizing it.
SecurityOrigin* origin = frame->securityContext()->getSecurityOrigin();
if (MixedContentChecker::isMixedContent(origin, url)) {
if (origin->protocol() != "https")
UseCounter::count(
frame,
UseCounter::MixedContentInNonHTTPSFrameThatRestrictsMixedContent);
} else if (!SecurityOrigin::isSecure(url) &&
SchemeRegistry::shouldTreatURLSchemeAsSecure(origin->protocol())) {
UseCounter::count(
frame,
UseCounter::MixedContentInSecureFrameThatDoesNotRestrictMixedContent);
}
}
bool requestIsSubframeSubresource(Frame* frame,
WebURLRequest::FrameType frameType) {
return (frame && frame != frame->tree().top() &&
frameType != WebURLRequest::FrameTypeNested);
}
// static
bool MixedContentChecker::isMixedContent(SecurityOrigin* securityOrigin,
const KURL& url) {
if (!SchemeRegistry::shouldTreatURLSchemeAsRestrictingMixedContent(
securityOrigin->protocol()))
return false;
// |url| is mixed content if its origin is not potentially trustworthy nor
// secure. We do a quick check against `SecurityOrigin::isSecure` to catch
// things like `about:blank`, which cannot be sanely passed into
// `SecurityOrigin::create` (as their origin depends on their context).
bool isAllowed = SecurityOrigin::isSecure(url) ||
SecurityOrigin::create(url)->isPotentiallyTrustworthy();
// TODO(mkwst): Remove this once 'localhost' is no longer considered
// potentially trustworthy.
if (isAllowed && url.protocolIs("http") &&
NetworkUtils::isLocalHostname(url.host(), nullptr))
isAllowed = false;
return !isAllowed;
}
// static
Frame* MixedContentChecker::inWhichFrameIsContentMixed(
Frame* frame,
WebURLRequest::FrameType frameType,
const KURL& url) {
// We only care about subresource loads; top-level navigations cannot be mixed
// content. Neither can frameless requests.
if (frameType == WebURLRequest::FrameTypeTopLevel || !frame)
return nullptr;
// Check the top frame first.
if (Frame* top = frame->tree().top()) {
measureStricterVersionOfIsMixedContent(top, url);
if (isMixedContent(top->securityContext()->getSecurityOrigin(), url))
return top;
}
measureStricterVersionOfIsMixedContent(frame, url);
if (isMixedContent(frame->securityContext()->getSecurityOrigin(), url))
return frame;
// No mixed content, no problem.
return nullptr;
}
// static
void MixedContentChecker::logToConsoleAboutFetch(
LocalFrame* frame,
const KURL& mainResourceUrl,
const KURL& url,
WebURLRequest::RequestContext requestContext,
bool allowed) {
String message = String::format(
"Mixed Content: The page at '%s' was loaded over HTTPS, but requested an "
"insecure %s '%s'. %s",
mainResourceUrl.elidedString().utf8().data(),
WebMixedContent::requestContextName(requestContext),
url.elidedString().utf8().data(),
allowed ? "This content should also be served over HTTPS."
: "This request has been blocked; the content must be served "
"over HTTPS.");
MessageLevel messageLevel = allowed ? WarningMessageLevel : ErrorMessageLevel;
frame->document()->addConsoleMessage(
ConsoleMessage::create(SecurityMessageSource, messageLevel, message));
}
// static
void MixedContentChecker::count(Frame* frame,
WebURLRequest::RequestContext requestContext) {
UseCounter::count(frame, UseCounter::MixedContentPresent);
// Roll blockable content up into a single counter, count unblocked types
// individually so we can determine when they can be safely moved to the
// blockable category:
WebMixedContent::ContextType contextType =
WebMixedContent::contextTypeFromRequestContext(
requestContext,
frame->settings()->strictMixedContentCheckingForPlugin());
if (contextType == WebMixedContent::ContextType::Blockable) {
UseCounter::count(frame, UseCounter::MixedContentBlockable);
return;
}
UseCounter::Feature feature;
switch (requestContext) {
case WebURLRequest::RequestContextAudio:
feature = UseCounter::MixedContentAudio;
break;
case WebURLRequest::RequestContextDownload:
feature = UseCounter::MixedContentDownload;
break;
case WebURLRequest::RequestContextFavicon:
feature = UseCounter::MixedContentFavicon;
break;
case WebURLRequest::RequestContextImage:
feature = UseCounter::MixedContentImage;
break;
case WebURLRequest::RequestContextInternal:
feature = UseCounter::MixedContentInternal;
break;
case WebURLRequest::RequestContextPlugin:
feature = UseCounter::MixedContentPlugin;
break;
case WebURLRequest::RequestContextPrefetch:
feature = UseCounter::MixedContentPrefetch;
break;
case WebURLRequest::RequestContextVideo:
feature = UseCounter::MixedContentVideo;
break;
default:
NOTREACHED();
return;
}
UseCounter::count(frame, feature);
}
// static
bool MixedContentChecker::shouldBlockFetch(
LocalFrame* frame,
WebURLRequest::RequestContext requestContext,
WebURLRequest::FrameType frameType,
ResourceRequest::RedirectStatus redirectStatus,
const KURL& url,
MixedContentChecker::ReportingStatus reportingStatus) {
Frame* effectiveFrame = effectiveFrameForFrameType(frame, frameType);
Frame* mixedFrame =
inWhichFrameIsContentMixed(effectiveFrame, frameType, url);
if (!mixedFrame)
return false;
MixedContentChecker::count(mixedFrame, requestContext);
if (ContentSecurityPolicy* policy =
frame->securityContext()->contentSecurityPolicy())
policy->reportMixedContent(url, redirectStatus);
Settings* settings = mixedFrame->settings();
// Use the current local frame's client; the embedder doesn't distinguish
// mixed content signals from different frames on the same page.
FrameLoaderClient* client = frame->loader().client();
SecurityOrigin* securityOrigin =
mixedFrame->securityContext()->getSecurityOrigin();
bool allowed = false;
// If we're in strict mode, we'll automagically fail everything, and
// intentionally skip the client checks in order to prevent degrading the
// site's security UI.
bool strictMode = mixedFrame->securityContext()->getInsecureRequestPolicy() &
kBlockAllMixedContent ||
settings->strictMixedContentChecking();
WebMixedContent::ContextType contextType =
WebMixedContent::contextTypeFromRequestContext(
requestContext, settings->strictMixedContentCheckingForPlugin());
// If we're loading the main resource of a subframe, we need to take a close
// look at the loaded URL. If we're dealing with a CORS-enabled scheme, then
// block mixed frames as active content. Otherwise, treat frames as passive
// content.
//
// FIXME: Remove this temporary hack once we have a reasonable API for
// launching external applications via URLs. http://crbug.com/318788 and
// https://crbug.com/393481
if (frameType == WebURLRequest::FrameTypeNested &&
!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(url.protocol()))
contextType = WebMixedContent::ContextType::OptionallyBlockable;
switch (contextType) {
case WebMixedContent::ContextType::OptionallyBlockable:
client->passiveInsecureContentFound(url);
allowed = !strictMode;
if (allowed)
client->didDisplayInsecureContent();
break;
case WebMixedContent::ContextType::Blockable: {
// Strictly block subresources that are mixed with respect to their
// subframes, unless all insecure content is allowed. This is to avoid the
// following situation: https://a.com embeds https://b.com, which loads a
// script over insecure HTTP. The user opts to allow the insecure content,
// thinking that they are allowing an insecure script to run on
// https://a.com and not realizing that they are in fact allowing an
// insecure script on https://b.com.
if (!settings->allowRunningOfInsecureContent() &&
requestIsSubframeSubresource(effectiveFrame, frameType) &&
isMixedContent(frame->securityContext()->getSecurityOrigin(), url)) {
UseCounter::count(mixedFrame,
UseCounter::BlockableMixedContentInSubframeBlocked);
allowed = false;
break;
}
bool shouldAskEmbedder =
!strictMode && settings &&
(!settings->strictlyBlockBlockableMixedContent() ||
settings->allowRunningOfInsecureContent());
allowed = shouldAskEmbedder &&
client->allowRunningInsecureContent(
settings && settings->allowRunningOfInsecureContent(),
securityOrigin, url);
if (allowed) {
client->didRunInsecureContent(securityOrigin, url);
UseCounter::count(mixedFrame, UseCounter::MixedContentBlockableAllowed);
}
break;
}
case WebMixedContent::ContextType::ShouldBeBlockable:
allowed = !strictMode;
if (allowed)
client->didDisplayInsecureContent();
break;
case WebMixedContent::ContextType::NotMixedContent:
NOTREACHED();
break;
};
if (reportingStatus == SendReport)
logToConsoleAboutFetch(frame, mainResourceUrlForFrame(mixedFrame), url,
requestContext, allowed);
return !allowed;
}
// static
void MixedContentChecker::logToConsoleAboutWebSocket(
LocalFrame* frame,
const KURL& mainResourceUrl,
const KURL& url,
bool allowed) {
String message = String::format(
"Mixed Content: The page at '%s' was loaded over HTTPS, but attempted to "
"connect to the insecure WebSocket endpoint '%s'. %s",
mainResourceUrl.elidedString().utf8().data(),
url.elidedString().utf8().data(),
allowed ? "This endpoint should be available via WSS. Insecure access is "
"deprecated."
: "This request has been blocked; this endpoint must be "
"available over WSS.");
MessageLevel messageLevel = allowed ? WarningMessageLevel : ErrorMessageLevel;
frame->document()->addConsoleMessage(
ConsoleMessage::create(SecurityMessageSource, messageLevel, message));
}
// static
bool MixedContentChecker::shouldBlockWebSocket(
LocalFrame* frame,
const KURL& url,
MixedContentChecker::ReportingStatus reportingStatus) {
Frame* mixedFrame =
inWhichFrameIsContentMixed(frame, WebURLRequest::FrameTypeNone, url);
if (!mixedFrame)
return false;
UseCounter::count(mixedFrame, UseCounter::MixedContentPresent);
UseCounter::count(mixedFrame, UseCounter::MixedContentWebSocket);
if (ContentSecurityPolicy* policy =
frame->securityContext()->contentSecurityPolicy())
policy->reportMixedContent(url,
ResourceRequest::RedirectStatus::NoRedirect);
Settings* settings = mixedFrame->settings();
// Use the current local frame's client; the embedder doesn't distinguish
// mixed content signals from different frames on the same page.
FrameLoaderClient* client = frame->loader().client();
SecurityOrigin* securityOrigin =
mixedFrame->securityContext()->getSecurityOrigin();
bool allowed = false;
// If we're in strict mode, we'll automagically fail everything, and
// intentionally skip the client checks in order to prevent degrading the
// site's security UI.
bool strictMode = mixedFrame->securityContext()->getInsecureRequestPolicy() &
kBlockAllMixedContent ||
settings->strictMixedContentChecking();
if (!strictMode) {
bool allowedPerSettings =
settings && settings->allowRunningOfInsecureContent();
allowed = client->allowRunningInsecureContent(allowedPerSettings,
securityOrigin, url);
}
if (allowed)
client->didRunInsecureContent(securityOrigin, url);
if (reportingStatus == SendReport)
logToConsoleAboutWebSocket(frame, mainResourceUrlForFrame(mixedFrame), url,
allowed);
return !allowed;
}
bool MixedContentChecker::isMixedFormAction(LocalFrame* frame,
const KURL& url,
ReportingStatus reportingStatus) {
// For whatever reason, some folks handle forms via JavaScript, and submit to
// `javascript:void(0)` rather than calling `preventDefault()`. We
// special-case `javascript:` URLs here, as they don't introduce MixedContent
// for form submissions.
if (url.protocolIs("javascript"))
return false;
Frame* mixedFrame =
inWhichFrameIsContentMixed(frame, WebURLRequest::FrameTypeNone, url);
if (!mixedFrame)
return false;
UseCounter::count(mixedFrame, UseCounter::MixedContentPresent);
// Use the current local frame's client; the embedder doesn't distinguish
// mixed content signals from different frames on the same page.
frame->loader().client()->didDisplayInsecureContent();
if (reportingStatus == SendReport) {
String message = String::format(
"Mixed Content: The page at '%s' was loaded over a secure connection, "
"but contains a form which targets an insecure endpoint '%s'. This "
"endpoint should be made available over a secure connection.",
mainResourceUrlForFrame(mixedFrame).elidedString().utf8().data(),
url.elidedString().utf8().data());
frame->document()->addConsoleMessage(ConsoleMessage::create(
SecurityMessageSource, WarningMessageLevel, message));
}
return true;
}
void MixedContentChecker::checkMixedPrivatePublic(
LocalFrame* frame,
const AtomicString& resourceIPAddress) {
if (!frame || !frame->document() || !frame->document()->loader())
return;
// Just count these for the moment, don't block them.
if (NetworkUtils::isReservedIPAddress(resourceIPAddress) &&
frame->document()->addressSpace() == WebAddressSpacePublic)
UseCounter::count(frame->document(),
UseCounter::MixedContentPrivateHostnameInPublicHostname);
}
Frame* MixedContentChecker::effectiveFrameForFrameType(
LocalFrame* frame,
WebURLRequest::FrameType frameType) {
// If we're loading the main resource of a subframe, ensure that we check
// against the parent of the active frame, rather than the frame itself.
if (frameType != WebURLRequest::FrameTypeNested)
return frame;
Frame* parentFrame = frame->tree().parent();
DCHECK(parentFrame);
return parentFrame;
}
void MixedContentChecker::handleCertificateError(
LocalFrame* frame,
const ResourceResponse& response,
WebURLRequest::FrameType frameType,
WebURLRequest::RequestContext requestContext) {
Frame* effectiveFrame = effectiveFrameForFrameType(frame, frameType);
if (frameType == WebURLRequest::FrameTypeTopLevel || !effectiveFrame)
return;
// Use the current local frame's client; the embedder doesn't distinguish
// mixed content signals from different frames on the same page.
FrameLoaderClient* client = frame->loader().client();
bool strictMixedContentCheckingForPlugin =
effectiveFrame->settings() &&
effectiveFrame->settings()->strictMixedContentCheckingForPlugin();
WebMixedContent::ContextType contextType =
WebMixedContent::contextTypeFromRequestContext(
requestContext, strictMixedContentCheckingForPlugin);
if (contextType == WebMixedContent::ContextType::Blockable) {
client->didRunContentWithCertificateErrors(response.url());
} else {
// contextTypeFromRequestContext() never returns NotMixedContent (it
// computes the type of mixed content, given that the content is mixed).
DCHECK_NE(contextType, WebMixedContent::ContextType::NotMixedContent);
client->didDisplayContentWithCertificateErrors(response.url());
}
}
WebMixedContent::ContextType MixedContentChecker::contextTypeForInspector(
LocalFrame* frame,
const ResourceRequest& request) {
Frame* effectiveFrame =
effectiveFrameForFrameType(frame, request.frameType());
Frame* mixedFrame = inWhichFrameIsContentMixed(
effectiveFrame, request.frameType(), request.url());
if (!mixedFrame)
return WebMixedContent::ContextType::NotMixedContent;
// See comment in shouldBlockFetch() about loading the main resource of a
// subframe.
if (request.frameType() == WebURLRequest::FrameTypeNested &&
!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(
request.url().protocol())) {
return WebMixedContent::ContextType::OptionallyBlockable;
}
bool strictMixedContentCheckingForPlugin =
mixedFrame->settings() &&
mixedFrame->settings()->strictMixedContentCheckingForPlugin();
return WebMixedContent::contextTypeFromRequestContext(
request.requestContext(), strictMixedContentCheckingForPlugin);
}
} // namespace blink