blob: bc0aff5204ccd168a9303cff4132c0724316e4a8 [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/weborigin/SchemeRegistry.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "public/platform/Platform.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()->securityOrigin()->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()->securityOrigin();
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;
// We're in a secure context, so |url| is mixed content if it's insecure.
return !SecurityOrigin::isSecure(url);
}
// 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()->securityOrigin(), url))
return top;
}
measureStricterVersionOfIsMixedContent(frame, url);
if (isMixedContent(frame->securityContext()->securityOrigin(), url))
return frame;
// No mixed content, no problem.
return nullptr;
}
// static
MixedContentChecker::ContextType MixedContentChecker::contextTypeFromContext(WebURLRequest::RequestContext context, Frame* frame)
{
switch (context) {
// "Optionally-blockable" mixed content
case WebURLRequest::RequestContextAudio:
case WebURLRequest::RequestContextFavicon:
case WebURLRequest::RequestContextImage:
case WebURLRequest::RequestContextVideo:
return ContextTypeOptionallyBlockable;
// Plugins! Oh how dearly we love plugin-loaded content!
case WebURLRequest::RequestContextPlugin: {
Settings* settings = frame->settings();
return settings && settings->strictMixedContentCheckingForPlugin() ? ContextTypeBlockable : ContextTypeOptionallyBlockable;
}
// "Blockable" mixed content
case WebURLRequest::RequestContextBeacon:
case WebURLRequest::RequestContextCSPReport:
case WebURLRequest::RequestContextEmbed:
case WebURLRequest::RequestContextEventSource:
case WebURLRequest::RequestContextFetch:
case WebURLRequest::RequestContextFont:
case WebURLRequest::RequestContextForm:
case WebURLRequest::RequestContextFrame:
case WebURLRequest::RequestContextHyperlink:
case WebURLRequest::RequestContextIframe:
case WebURLRequest::RequestContextImageSet:
case WebURLRequest::RequestContextImport:
case WebURLRequest::RequestContextLocation:
case WebURLRequest::RequestContextManifest:
case WebURLRequest::RequestContextObject:
case WebURLRequest::RequestContextPing:
case WebURLRequest::RequestContextScript:
case WebURLRequest::RequestContextServiceWorker:
case WebURLRequest::RequestContextSharedWorker:
case WebURLRequest::RequestContextStyle:
case WebURLRequest::RequestContextSubresource:
case WebURLRequest::RequestContextTrack:
case WebURLRequest::RequestContextWorker:
case WebURLRequest::RequestContextXMLHttpRequest:
case WebURLRequest::RequestContextXSLT:
return ContextTypeBlockable;
// FIXME: Contexts that we should block, but don't currently. https://crbug.com/388650
case WebURLRequest::RequestContextDownload:
case WebURLRequest::RequestContextInternal:
case WebURLRequest::RequestContextPrefetch:
return ContextTypeShouldBeBlockable;
case WebURLRequest::RequestContextUnspecified:
ASSERT_NOT_REACHED();
}
ASSERT_NOT_REACHED();
return ContextTypeBlockable;
}
// static
const char* MixedContentChecker::typeNameFromContext(WebURLRequest::RequestContext context)
{
switch (context) {
case WebURLRequest::RequestContextAudio:
return "audio file";
case WebURLRequest::RequestContextBeacon:
return "Beacon endpoint";
case WebURLRequest::RequestContextCSPReport:
return "Content Security Policy reporting endpoint";
case WebURLRequest::RequestContextDownload:
return "download";
case WebURLRequest::RequestContextEmbed:
return "plugin resource";
case WebURLRequest::RequestContextEventSource:
return "EventSource endpoint";
case WebURLRequest::RequestContextFavicon:
return "favicon";
case WebURLRequest::RequestContextFetch:
return "resource";
case WebURLRequest::RequestContextFont:
return "font";
case WebURLRequest::RequestContextForm:
return "form action";
case WebURLRequest::RequestContextFrame:
return "frame";
case WebURLRequest::RequestContextHyperlink:
return "resource";
case WebURLRequest::RequestContextIframe:
return "frame";
case WebURLRequest::RequestContextImage:
return "image";
case WebURLRequest::RequestContextImageSet:
return "image";
case WebURLRequest::RequestContextImport:
return "HTML Import";
case WebURLRequest::RequestContextInternal:
return "resource";
case WebURLRequest::RequestContextLocation:
return "resource";
case WebURLRequest::RequestContextManifest:
return "manifest";
case WebURLRequest::RequestContextObject:
return "plugin resource";
case WebURLRequest::RequestContextPing:
return "hyperlink auditing endpoint";
case WebURLRequest::RequestContextPlugin:
return "plugin data";
case WebURLRequest::RequestContextPrefetch:
return "prefetch resource";
case WebURLRequest::RequestContextScript:
return "script";
case WebURLRequest::RequestContextServiceWorker:
return "Service Worker script";
case WebURLRequest::RequestContextSharedWorker:
return "Shared Worker script";
case WebURLRequest::RequestContextStyle:
return "stylesheet";
case WebURLRequest::RequestContextSubresource:
return "resource";
case WebURLRequest::RequestContextTrack:
return "Text Track";
case WebURLRequest::RequestContextUnspecified:
return "resource";
case WebURLRequest::RequestContextVideo:
return "video";
case WebURLRequest::RequestContextWorker:
return "Worker script";
case WebURLRequest::RequestContextXMLHttpRequest:
return "XMLHttpRequest endpoint";
case WebURLRequest::RequestContextXSLT:
return "XSLT";
}
ASSERT_NOT_REACHED();
return "resource";
}
// 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(), typeNameFromContext(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:
ContextType contextType = contextTypeFromContext(requestContext, frame);
if (contextType == ContextTypeBlockable) {
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:
ASSERT_NOT_REACHED();
return;
}
UseCounter::count(frame, feature);
}
// static
bool MixedContentChecker::shouldBlockFetch(LocalFrame* frame, WebURLRequest::RequestContext requestContext, WebURLRequest::FrameType frameType, 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);
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()->securityOrigin();
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()->shouldEnforceStrictMixedContentChecking() || settings->strictMixedContentChecking();
ContextType contextType = contextTypeFromContext(requestContext, mixedFrame);
// 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 = ContextTypeOptionallyBlockable;
switch (contextType) {
case ContextTypeOptionallyBlockable:
allowed = !strictMode && client->allowDisplayingInsecureContent(settings && settings->allowDisplayOfInsecureContent(), url);
if (allowed)
client->didDisplayInsecureContent();
break;
case ContextTypeBlockable: {
// 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()->securityOrigin(), 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 ContextTypeShouldBeBlockable:
allowed = !strictMode;
if (allowed)
client->didDisplayInsecureContent();
break;
case ContextTypeNotMixedContent:
ASSERT_NOT_REACHED();
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);
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()->securityOrigin();
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()->shouldEnforceStrictMixedContentChecking() || 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 (Platform::current()->isReservedIPAddress(resourceIPAddress) && !frame->document()->isHostedInReservedIPRange())
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();
ASSERT(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;
// TODO(estark): handle remote frames, perhaps by omitting security info when the effective frame is remote.
if (!effectiveFrame->isLocalFrame())
return;
LocalFrame* localEffectiveFrame = toLocalFrame(effectiveFrame);
// 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();
ContextType contextType = MixedContentChecker::contextTypeFromContext(requestContext, effectiveFrame);
if (contextType == ContextTypeBlockable) {
client->didRunContentWithCertificateErrors(response.url(), response.getSecurityInfo(), mainResourceUrlForFrame(effectiveFrame), localEffectiveFrame->loader().documentLoader()->response().getSecurityInfo());
} else {
// contextTypeFromContext() never returns NotMixedContent (it
// computes the type of mixed content, given that the content is
// mixed).
ASSERT(contextType != ContextTypeNotMixedContent);
client->didDisplayContentWithCertificateErrors(response.url(), response.getSecurityInfo(), mainResourceUrlForFrame(effectiveFrame), localEffectiveFrame->loader().documentLoader()->response().getSecurityInfo());
}
}
MixedContentChecker::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 ContextTypeNotMixedContent;
// See comment in shouldBlockFetch() about loading the main resource of a subframe.
if (request.frameType() == WebURLRequest::FrameTypeNested && !SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(request.url().protocol())) {
return ContextTypeOptionallyBlockable;
}
return contextTypeFromContext(request.requestContext(), mixedFrame);
}
} // namespace blink