| /* |
| * 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 |