| /* |
| * 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 "third_party/blink/renderer/core/loader/mixed_content_checker.h" |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "services/network/public/mojom/ip_address_space.mojom-blink.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/security_context/insecure_request_policy.h" |
| #include "third_party/blink/public/mojom/devtools/inspector_issue.mojom-blink.h" |
| #include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h" |
| #include "third_party/blink/public/mojom/security_context/insecure_request_policy.mojom-blink.h" |
| #include "third_party/blink/public/platform/web_content_settings_client.h" |
| #include "third_party/blink/public/platform/web_mixed_content.h" |
| #include "third_party/blink/public/platform/web_security_origin.h" |
| #include "third_party/blink/public/platform/web_worker_fetch_context.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/frame/frame.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_client.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/core/loader/frame_fetch_context.h" |
| #include "third_party/blink/renderer/core/loader/worker_fetch_context.h" |
| #include "third_party/blink/renderer/core/probe/core_probes.h" |
| #include "third_party/blink/renderer/core/workers/worker_global_scope.h" |
| #include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h" |
| #include "third_party/blink/renderer/core/workers/worker_settings.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher_properties.h" |
| #include "third_party/blink/renderer/platform/network/network_utils.h" |
| #include "third_party/blink/renderer/platform/weborigin/scheme_registry.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_builder.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(NullURL(), |
| frame->GetSecurityContext()->GetSecurityOrigin()->ToString()); |
| } |
| return To<LocalFrame>(frame)->GetDocument()->Url(); |
| } |
| |
| const char* RequestContextName(mojom::RequestContextType context) { |
| switch (context) { |
| case mojom::RequestContextType::AUDIO: |
| return "audio file"; |
| case mojom::RequestContextType::BEACON: |
| return "Beacon endpoint"; |
| case mojom::RequestContextType::CSP_REPORT: |
| return "Content Security Policy reporting endpoint"; |
| case mojom::RequestContextType::DOWNLOAD: |
| return "download"; |
| case mojom::RequestContextType::EMBED: |
| return "plugin resource"; |
| case mojom::RequestContextType::EVENT_SOURCE: |
| return "EventSource endpoint"; |
| case mojom::RequestContextType::FAVICON: |
| return "favicon"; |
| case mojom::RequestContextType::FETCH: |
| return "resource"; |
| case mojom::RequestContextType::FONT: |
| return "font"; |
| case mojom::RequestContextType::FORM: |
| return "form action"; |
| case mojom::RequestContextType::FRAME: |
| return "frame"; |
| case mojom::RequestContextType::HYPERLINK: |
| return "resource"; |
| case mojom::RequestContextType::IFRAME: |
| return "frame"; |
| case mojom::RequestContextType::IMAGE: |
| return "image"; |
| case mojom::RequestContextType::IMAGE_SET: |
| return "image"; |
| case mojom::RequestContextType::IMPORT: |
| return "HTML Import"; |
| case mojom::RequestContextType::INTERNAL: |
| return "resource"; |
| case mojom::RequestContextType::LOCATION: |
| return "resource"; |
| case mojom::RequestContextType::MANIFEST: |
| return "manifest"; |
| case mojom::RequestContextType::OBJECT: |
| return "plugin resource"; |
| case mojom::RequestContextType::PING: |
| return "hyperlink auditing endpoint"; |
| case mojom::RequestContextType::PLUGIN: |
| return "plugin data"; |
| case mojom::RequestContextType::PREFETCH: |
| return "prefetch resource"; |
| case mojom::RequestContextType::SCRIPT: |
| return "script"; |
| case mojom::RequestContextType::SERVICE_WORKER: |
| return "Service Worker script"; |
| case mojom::RequestContextType::SHARED_WORKER: |
| return "Shared Worker script"; |
| case mojom::RequestContextType::STYLE: |
| return "stylesheet"; |
| case mojom::RequestContextType::SUBRESOURCE: |
| return "resource"; |
| case mojom::RequestContextType::TRACK: |
| return "Text Track"; |
| case mojom::RequestContextType::UNSPECIFIED: |
| return "resource"; |
| case mojom::RequestContextType::VIDEO: |
| return "video"; |
| case mojom::RequestContextType::WORKER: |
| return "Worker script"; |
| case mojom::RequestContextType::XML_HTTP_REQUEST: |
| return "XMLHttpRequest endpoint"; |
| case mojom::RequestContextType::XSLT: |
| return "XSLT"; |
| } |
| NOTREACHED(); |
| return "resource"; |
| } |
| |
| // Currently we have two slightly different versions, because |
| // in frames SecurityContext is the source of CSP/InsecureRequestPolicy, |
| // especially where FetchContext and SecurityContext come from different |
| // frames (e.g. in nested frames), while in |
| // workers we should totally rely on FetchContext's FetchClientSettingsObject |
| // to avoid confusion around off-the-main-thread fetch. |
| // TODO(hiroshige): Consider merging them once FetchClientSettingsObject |
| // becomes the source of CSP/InsecureRequestPolicy also in frames. |
| bool IsWebSocketAllowedInFrame(const BaseFetchContext& fetch_context, |
| const SecurityContext* security_context, |
| Settings* settings, |
| const KURL& url) { |
| fetch_context.CountUsage(WebFeature::kMixedContentPresent); |
| fetch_context.CountUsage(WebFeature::kMixedContentWebSocket); |
| if (ContentSecurityPolicy* policy = |
| security_context->GetContentSecurityPolicy()) { |
| policy->ReportMixedContent(url, |
| ResourceRequest::RedirectStatus::kNoRedirect); |
| } |
| |
| // 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 strict_mode = |
| (security_context->GetInsecureRequestPolicy() & |
| mojom::blink::InsecureRequestPolicy::kBlockAllMixedContent) != |
| mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone || |
| settings->GetStrictMixedContentChecking(); |
| if (strict_mode) |
| return false; |
| return settings && settings->GetAllowRunningOfInsecureContent(); |
| } |
| |
| bool IsWebSocketAllowedInWorker(const WorkerFetchContext& fetch_context, |
| WorkerSettings* settings, |
| const KURL& url) { |
| fetch_context.CountUsage(WebFeature::kMixedContentPresent); |
| fetch_context.CountUsage(WebFeature::kMixedContentWebSocket); |
| if (const ContentSecurityPolicy* policy = |
| fetch_context.GetContentSecurityPolicy()) { |
| policy->ReportMixedContent(url, |
| ResourceRequest::RedirectStatus::kNoRedirect); |
| } |
| |
| // 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 strict_mode = |
| (fetch_context.GetResourceFetcherProperties() |
| .GetFetchClientSettingsObject() |
| .GetInsecureRequestsPolicy() & |
| mojom::blink::InsecureRequestPolicy::kBlockAllMixedContent) != |
| mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone || |
| settings->GetStrictMixedContentChecking(); |
| if (strict_mode) |
| return false; |
| return settings && settings->GetAllowRunningOfInsecureContent(); |
| } |
| |
| void CreateMixedContentIssue( |
| const KURL& main_resource_url, |
| const KURL& insecure_url, |
| const mojom::blink::RequestContextType request_context, |
| LocalFrame* frame, |
| const mojom::blink::MixedContentResolutionStatus resolution_status, |
| const base::Optional<String>& devtools_id) { |
| auto mixedContent = mojom::blink::MixedContentIssueDetails::New(); |
| mixedContent->request_context = request_context, |
| mixedContent->resolution_status = resolution_status; |
| mixedContent->insecure_url = insecure_url.GetString(); |
| mixedContent->main_resource_url = main_resource_url.GetString(); |
| |
| if (devtools_id) { |
| auto affected_request = mojom::blink::AffectedRequest::New(); |
| affected_request->request_id = *devtools_id; |
| affected_request->url = insecure_url.GetString(); |
| mixedContent->request = std::move(affected_request); |
| } |
| |
| auto affected_frame = mojom::blink::AffectedFrame::New(); |
| affected_frame->frame_id = frame->GetDevToolsFrameToken().ToString().c_str(); |
| mixedContent->frame = std::move(affected_frame); |
| |
| auto details = mojom::blink::InspectorIssueDetails::New(); |
| details->mixed_content_issue_details = std::move(mixedContent); |
| |
| frame->AddInspectorIssue(mojom::blink::InspectorIssueInfo::New( |
| mojom::blink::InspectorIssueCode::kMixedContentIssue, |
| std::move(details))); |
| } |
| |
| } // namespace |
| |
| static void MeasureStricterVersionOfIsMixedContent(Frame& frame, |
| const KURL& url, |
| const LocalFrame* source) { |
| // 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. |
| const SecurityOrigin* origin = |
| frame.GetSecurityContext()->GetSecurityOrigin(); |
| if (MixedContentChecker::IsMixedContent(origin, url)) { |
| if (origin->Protocol() != "https") { |
| UseCounter::Count( |
| source->GetDocument(), |
| WebFeature::kMixedContentInNonHTTPSFrameThatRestrictsMixedContent); |
| } |
| } else if (!SecurityOrigin::IsSecure(url) && |
| SchemeRegistry::ShouldTreatURLSchemeAsSecure(origin->Protocol())) { |
| UseCounter::Count( |
| source->GetDocument(), |
| WebFeature::kMixedContentInSecureFrameThatDoesNotRestrictMixedContent); |
| } |
| } |
| |
| bool RequestIsSubframeSubresource(Frame* frame) { |
| return frame && frame != frame->Tree().Top(); |
| } |
| |
| static bool IsInsecureUrl(const KURL& url) { |
| // |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). |
| // blob: and filesystem: URLs never hit the network, and access is restricted |
| // to same-origin contexts, so they are not blocked either. |
| bool is_allowed = url.ProtocolIs("blob") || url.ProtocolIs("filesystem") || |
| SecurityOrigin::IsSecure(url) || |
| SecurityOrigin::Create(url)->IsPotentiallyTrustworthy(); |
| return !is_allowed; |
| } |
| |
| // static |
| bool MixedContentChecker::IsMixedContent(const SecurityOrigin* security_origin, |
| const KURL& url) { |
| return IsMixedContent(security_origin->Protocol(), url); |
| } |
| |
| // static |
| bool MixedContentChecker::IsMixedContent(const String& origin_protocol, |
| const KURL& url) { |
| if (!SchemeRegistry::ShouldTreatURLSchemeAsRestrictingMixedContent( |
| origin_protocol)) |
| return false; |
| |
| return IsInsecureUrl(url); |
| } |
| |
| // static |
| bool MixedContentChecker::IsMixedContent( |
| const FetchClientSettingsObject& settings, |
| const KURL& url) { |
| switch (settings.GetHttpsState()) { |
| case HttpsState::kNone: |
| return false; |
| |
| case HttpsState::kModern: |
| return IsInsecureUrl(url); |
| } |
| } |
| |
| // static |
| Frame* MixedContentChecker::InWhichFrameIsContentMixed(LocalFrame* frame, |
| const KURL& url) { |
| // Frameless requests cannot be mixed content. |
| if (!frame) |
| return nullptr; |
| |
| // Check the top frame first. |
| Frame& top = frame->Tree().Top(); |
| MeasureStricterVersionOfIsMixedContent(top, url, frame); |
| if (IsMixedContent(top.GetSecurityContext()->GetSecurityOrigin(), url)) |
| return ⊤ |
| |
| MeasureStricterVersionOfIsMixedContent(*frame, url, frame); |
| if (IsMixedContent(frame->GetSecurityContext()->GetSecurityOrigin(), url)) |
| return frame; |
| |
| // No mixed content, no problem. |
| return nullptr; |
| } |
| |
| // static |
| ConsoleMessage* MixedContentChecker::CreateConsoleMessageAboutFetch( |
| const KURL& main_resource_url, |
| const KURL& url, |
| mojom::RequestContextType request_context, |
| bool allowed, |
| std::unique_ptr<SourceLocation> source_location) { |
| String message = String::Format( |
| "Mixed Content: The page at '%s' was loaded over HTTPS, but requested an " |
| "insecure %s '%s'. %s", |
| main_resource_url.ElidedString().Utf8().c_str(), |
| RequestContextName(request_context), url.ElidedString().Utf8().c_str(), |
| allowed ? "This content should also be served over HTTPS." |
| : "This request has been blocked; the content must be served " |
| "over HTTPS."); |
| mojom::ConsoleMessageLevel message_level = |
| allowed ? mojom::ConsoleMessageLevel::kWarning |
| : mojom::ConsoleMessageLevel::kError; |
| if (source_location) { |
| return MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kSecurity, message_level, message, |
| std::move(source_location)); |
| } |
| return MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kSecurity, message_level, message); |
| } |
| |
| // static |
| void MixedContentChecker::Count(Frame* frame, |
| mojom::RequestContextType request_context, |
| const LocalFrame* source) { |
| UseCounter::Count(source->GetDocument(), WebFeature::kMixedContentPresent); |
| |
| // 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: |
| WebMixedContentContextType context_type = |
| WebMixedContent::ContextTypeFromRequestContext( |
| request_context, |
| frame->GetSettings()->GetStrictMixedContentCheckingForPlugin()); |
| if (context_type == WebMixedContentContextType::kBlockable) { |
| UseCounter::Count(source->GetDocument(), |
| WebFeature::kMixedContentBlockable); |
| return; |
| } |
| |
| WebFeature feature; |
| switch (request_context) { |
| case mojom::RequestContextType::AUDIO: |
| feature = WebFeature::kMixedContentAudio; |
| break; |
| case mojom::RequestContextType::DOWNLOAD: |
| feature = WebFeature::kMixedContentDownload; |
| break; |
| case mojom::RequestContextType::FAVICON: |
| feature = WebFeature::kMixedContentFavicon; |
| break; |
| case mojom::RequestContextType::IMAGE: |
| feature = WebFeature::kMixedContentImage; |
| break; |
| case mojom::RequestContextType::INTERNAL: |
| feature = WebFeature::kMixedContentInternal; |
| break; |
| case mojom::RequestContextType::PLUGIN: |
| feature = WebFeature::kMixedContentPlugin; |
| break; |
| case mojom::RequestContextType::PREFETCH: |
| feature = WebFeature::kMixedContentPrefetch; |
| break; |
| case mojom::RequestContextType::VIDEO: |
| feature = WebFeature::kMixedContentVideo; |
| break; |
| |
| default: |
| NOTREACHED(); |
| return; |
| } |
| UseCounter::Count(source->GetDocument(), feature); |
| } |
| |
| // static |
| bool MixedContentChecker::ShouldBlockFetch( |
| LocalFrame* frame, |
| mojom::RequestContextType request_context, |
| const KURL& url_before_redirects, |
| ResourceRequest::RedirectStatus redirect_status, |
| const KURL& url, |
| const base::Optional<String>& devtools_id, |
| ReportingDisposition reporting_disposition, |
| mojom::blink::ContentSecurityNotifier& notifier) { |
| Frame* mixed_frame = InWhichFrameIsContentMixed(frame, url); |
| if (!mixed_frame) |
| return false; |
| |
| // Exempt non-webby schemes from mixed content treatment. For subresources, |
| // these will be blocked anyway as net::ERR_UNKNOWN_URL_SCHEME, so there's no |
| // need to present a security warning. Non-webby main resources (including |
| // subframes) are handled in the browser process's mixed content checking, |
| // where the URL will be allowed to load, but not treated as mixed content |
| // because it can't return data to the browser. See https://crbug.com/621131. |
| // |
| // TODO(https://crbug.com/1030307): decide whether CORS-enabled is really the |
| // right way to draw this distinction. |
| if (!SchemeRegistry::ShouldTreatURLSchemeAsCorsEnabled(url.Protocol())) { |
| // 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 IsInsecureUrl() and cause an early-return because of the |
| // InWhichFrameIsContentMixed() check above. |
| UseCounter::Count(frame->GetDocument(), WebFeature::kNonWebbyMixedContent); |
| return false; |
| } |
| |
| MixedContentChecker::Count(mixed_frame, request_context, frame); |
| if (ContentSecurityPolicy* policy = |
| frame->GetSecurityContext()->GetContentSecurityPolicy()) |
| policy->ReportMixedContent(url_before_redirects, redirect_status); |
| |
| Settings* settings = mixed_frame->GetSettings(); |
| auto& local_frame_host = frame->GetLocalFrameHostRemote(); |
| WebContentSettingsClient* content_settings_client = |
| frame->GetContentSettingsClient(); |
| const SecurityOrigin* security_origin = |
| mixed_frame->GetSecurityContext()->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 strict_mode = |
| (mixed_frame->GetSecurityContext()->GetInsecureRequestPolicy() & |
| mojom::blink::InsecureRequestPolicy::kBlockAllMixedContent) != |
| mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone || |
| settings->GetStrictMixedContentChecking(); |
| |
| WebMixedContentContextType context_type = |
| WebMixedContent::ContextTypeFromRequestContext( |
| request_context, settings->GetStrictMixedContentCheckingForPlugin()); |
| |
| switch (context_type) { |
| case WebMixedContentContextType::kOptionallyBlockable: |
| allowed = !strict_mode; |
| if (allowed) { |
| if (content_settings_client) |
| content_settings_client->PassiveInsecureContentFound(url); |
| local_frame_host.DidDisplayInsecureContent(); |
| } |
| break; |
| |
| case WebMixedContentContextType::kBlockable: { |
| // 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->GetAllowRunningOfInsecureContent() && |
| RequestIsSubframeSubresource(frame) && |
| IsMixedContent(frame->GetSecurityContext()->GetSecurityOrigin(), |
| url)) { |
| UseCounter::Count(frame->GetDocument(), |
| WebFeature::kBlockableMixedContentInSubframeBlocked); |
| allowed = false; |
| break; |
| } |
| |
| bool should_ask_embedder = |
| !strict_mode && settings && |
| (!settings->GetStrictlyBlockBlockableMixedContent() || |
| settings->GetAllowRunningOfInsecureContent()); |
| if (should_ask_embedder) { |
| allowed = settings && settings->GetAllowRunningOfInsecureContent(); |
| if (content_settings_client) { |
| allowed = content_settings_client->AllowRunningInsecureContent( |
| allowed, url); |
| } |
| } |
| if (allowed) { |
| notifier.NotifyInsecureContentRan(KURL(security_origin->ToString()), |
| url); |
| UseCounter::Count(frame->GetDocument(), |
| WebFeature::kMixedContentBlockableAllowed); |
| } |
| break; |
| } |
| |
| case WebMixedContentContextType::kShouldBeBlockable: |
| allowed = !strict_mode; |
| if (allowed) |
| local_frame_host.DidDisplayInsecureContent(); |
| break; |
| case WebMixedContentContextType::kNotMixedContent: |
| NOTREACHED(); |
| break; |
| }; |
| |
| if (reporting_disposition == ReportingDisposition::kReport) { |
| frame->GetDocument()->AddConsoleMessage( |
| CreateConsoleMessageAboutFetch(MainResourceUrlForFrame(mixed_frame), |
| url, request_context, allowed, nullptr)); |
| } |
| // Issue is created even when reporting disposition is false i.e. for |
| // speculative prefetches. Otherwise the DevTools frontend would not |
| // receive an issue with a devtools_id which it can match to a request. |
| CreateMixedContentIssue( |
| MainResourceUrlForFrame(mixed_frame), url, request_context, frame, |
| allowed |
| ? mojom::blink::MixedContentResolutionStatus::kMixedContentWarning |
| : mojom::blink::MixedContentResolutionStatus::kMixedContentBlocked, |
| devtools_id); |
| return !allowed; |
| } |
| |
| // static |
| bool MixedContentChecker::ShouldBlockFetchOnWorker( |
| WorkerFetchContext& worker_fetch_context, |
| mojom::RequestContextType request_context, |
| const KURL& url_before_redirects, |
| ResourceRequest::RedirectStatus redirect_status, |
| const KURL& url, |
| ReportingDisposition reporting_disposition, |
| bool is_worklet_global_scope) { |
| const FetchClientSettingsObject& fetch_client_settings_object = |
| worker_fetch_context.GetResourceFetcherProperties() |
| .GetFetchClientSettingsObject(); |
| if (!MixedContentChecker::IsMixedContent(fetch_client_settings_object, url)) { |
| return false; |
| } |
| |
| worker_fetch_context.CountUsage(WebFeature::kMixedContentPresent); |
| worker_fetch_context.CountUsage(WebFeature::kMixedContentBlockable); |
| if (auto* policy = worker_fetch_context.GetContentSecurityPolicy()) |
| policy->ReportMixedContent(url_before_redirects, redirect_status); |
| |
| // Blocks all mixed content request from worklets. |
| // TODO(horo): Revise this when the spec is updated. |
| // Worklets spec: https://www.w3.org/TR/worklets-1/#security-considerations |
| // Spec issue: https://github.com/w3c/css-houdini-drafts/issues/92 |
| if (is_worklet_global_scope) |
| return true; |
| |
| WorkerSettings* settings = worker_fetch_context.GetWorkerSettings(); |
| DCHECK(settings); |
| bool allowed = false; |
| if (!settings->GetAllowRunningOfInsecureContent() && |
| worker_fetch_context.GetWebWorkerFetchContext()->IsOnSubframe()) { |
| worker_fetch_context.CountUsage( |
| WebFeature::kBlockableMixedContentInSubframeBlocked); |
| allowed = false; |
| } else { |
| bool strict_mode = |
| (fetch_client_settings_object.GetInsecureRequestsPolicy() & |
| mojom::blink::InsecureRequestPolicy::kBlockAllMixedContent) != |
| mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone || |
| settings->GetStrictMixedContentChecking(); |
| bool should_ask_embedder = |
| !strict_mode && (!settings->GetStrictlyBlockBlockableMixedContent() || |
| settings->GetAllowRunningOfInsecureContent()); |
| allowed = should_ask_embedder && |
| worker_fetch_context.AllowRunningInsecureContent( |
| settings->GetAllowRunningOfInsecureContent(), url); |
| if (allowed) { |
| worker_fetch_context.GetContentSecurityNotifier() |
| .NotifyInsecureContentRan( |
| KURL( |
| fetch_client_settings_object.GetSecurityOrigin()->ToString()), |
| url); |
| worker_fetch_context.CountUsage( |
| WebFeature::kMixedContentBlockableAllowed); |
| } |
| } |
| |
| if (reporting_disposition == ReportingDisposition::kReport) { |
| worker_fetch_context.AddConsoleMessage(CreateConsoleMessageAboutFetch( |
| worker_fetch_context.Url(), url, request_context, allowed, nullptr)); |
| } |
| return !allowed; |
| } |
| |
| // static |
| ConsoleMessage* MixedContentChecker::CreateConsoleMessageAboutWebSocket( |
| const KURL& main_resource_url, |
| 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", |
| main_resource_url.ElidedString().Utf8().c_str(), |
| url.ElidedString().Utf8().c_str(), |
| allowed ? "This endpoint should be available via WSS. Insecure access is " |
| "deprecated." |
| : "This request has been blocked; this endpoint must be " |
| "available over WSS."); |
| mojom::ConsoleMessageLevel message_level = |
| allowed ? mojom::ConsoleMessageLevel::kWarning |
| : mojom::ConsoleMessageLevel::kError; |
| return MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kSecurity, message_level, message); |
| } |
| |
| // static |
| bool MixedContentChecker::IsWebSocketAllowed( |
| const FrameFetchContext& frame_fetch_context, |
| LocalFrame* frame, |
| const KURL& url) { |
| Frame* mixed_frame = InWhichFrameIsContentMixed(frame, url); |
| if (!mixed_frame) |
| return true; |
| |
| Settings* settings = mixed_frame->GetSettings(); |
| // Use the current local frame's client; the embedder doesn't distinguish |
| // mixed content signals from different frames on the same page. |
| WebContentSettingsClient* content_settings_client = |
| frame->GetContentSettingsClient(); |
| const SecurityContext* security_context = mixed_frame->GetSecurityContext(); |
| const SecurityOrigin* security_origin = security_context->GetSecurityOrigin(); |
| |
| bool allowed = IsWebSocketAllowedInFrame(frame_fetch_context, |
| security_context, settings, url); |
| if (content_settings_client) { |
| allowed = |
| content_settings_client->AllowRunningInsecureContent(allowed, url); |
| } |
| |
| if (allowed) { |
| frame_fetch_context.GetContentSecurityNotifier().NotifyInsecureContentRan( |
| KURL(security_origin->ToString()), url); |
| } |
| |
| frame->GetDocument()->AddConsoleMessage(CreateConsoleMessageAboutWebSocket( |
| MainResourceUrlForFrame(mixed_frame), url, allowed)); |
| CreateMixedContentIssue( |
| MainResourceUrlForFrame(mixed_frame), url, |
| mojom::blink::RequestContextType::FETCH, frame, |
| allowed |
| ? mojom::blink::MixedContentResolutionStatus::kMixedContentWarning |
| : mojom::blink::MixedContentResolutionStatus::kMixedContentBlocked, |
| base::Optional<String>()); |
| return allowed; |
| } |
| |
| // static |
| bool MixedContentChecker::IsWebSocketAllowed( |
| WorkerFetchContext& worker_fetch_context, |
| const KURL& url) { |
| const FetchClientSettingsObject& fetch_client_settings_object = |
| worker_fetch_context.GetResourceFetcherProperties() |
| .GetFetchClientSettingsObject(); |
| if (!MixedContentChecker::IsMixedContent(fetch_client_settings_object, url)) { |
| return true; |
| } |
| |
| WorkerSettings* settings = worker_fetch_context.GetWorkerSettings(); |
| const SecurityOrigin* security_origin = |
| fetch_client_settings_object.GetSecurityOrigin(); |
| |
| bool allowed = |
| IsWebSocketAllowedInWorker(worker_fetch_context, settings, url); |
| allowed = worker_fetch_context.AllowRunningInsecureContent(allowed, url); |
| |
| if (allowed) { |
| worker_fetch_context.GetContentSecurityNotifier().NotifyInsecureContentRan( |
| KURL(security_origin->ToString()), url); |
| } |
| |
| worker_fetch_context.AddConsoleMessage(CreateConsoleMessageAboutWebSocket( |
| worker_fetch_context.Url(), url, allowed)); |
| |
| return allowed; |
| } |
| |
| bool MixedContentChecker::IsMixedFormAction( |
| LocalFrame* frame, |
| const KURL& url, |
| ReportingDisposition reporting_disposition) { |
| // 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* mixed_frame = InWhichFrameIsContentMixed(frame, url); |
| if (!mixed_frame) |
| return false; |
| |
| UseCounter::Count(frame->GetDocument(), WebFeature::kMixedContentPresent); |
| |
| // Use the current local frame's client; the embedder doesn't distinguish |
| // mixed content signals from different frames on the same page. |
| frame->GetLocalFrameHostRemote().DidContainInsecureFormAction(); |
| |
| if (reporting_disposition == ReportingDisposition::kReport) { |
| String message = String::Format( |
| "Mixed Content: The page at '%s' was loaded over a secure connection, " |
| "but contains a form that targets an insecure endpoint '%s'. This " |
| "endpoint should be made available over a secure connection.", |
| MainResourceUrlForFrame(mixed_frame).ElidedString().Utf8().c_str(), |
| url.ElidedString().Utf8().c_str()); |
| frame->GetDocument()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kSecurity, |
| mojom::ConsoleMessageLevel::kWarning, message)); |
| } |
| // Issue is created even when reporting disposition is false i.e. for |
| // speculative prefetches. Otherwise the DevTools frontend would not |
| // receive an issue with a devtools_id which it can match to a request. |
| CreateMixedContentIssue( |
| MainResourceUrlForFrame(mixed_frame), url, |
| mojom::blink::RequestContextType::FORM, frame, |
| mojom::blink::MixedContentResolutionStatus::kMixedContentWarning, |
| base::Optional<String>()); |
| |
| return true; |
| } |
| |
| bool MixedContentChecker::ShouldAutoupgrade( |
| HttpsState context_https_state, |
| mojom::RequestContextType type, |
| WebContentSettingsClient* settings_client, |
| const KURL& url) { |
| // We are currently not autoupgrading plugin loaded content, which is why |
| // strict_mixed_content_for_plugin is hardcoded to true. |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kMixedContentAutoupgrade) || |
| context_https_state == HttpsState::kNone || |
| WebMixedContent::ContextTypeFromRequestContext(type, true) != |
| WebMixedContentContextType::kOptionallyBlockable) { |
| return false; |
| } |
| if (settings_client && !settings_client->ShouldAutoupgradeMixedContent()) { |
| return false; |
| } |
| |
| auto autoupgrade_mode = base::GetFieldTrialParamValueByFeature( |
| blink::features::kMixedContentAutoupgrade, |
| blink::features::kMixedContentAutoupgradeModeParamName); |
| |
| if (autoupgrade_mode == |
| blink::features::kMixedContentAutoupgradeModeAllPassive) { |
| return true; |
| } |
| |
| // Otherwise we default to excluding images. |
| return type != mojom::RequestContextType::IMAGE; |
| } |
| |
| void MixedContentChecker::CheckMixedPrivatePublic( |
| LocalFrame* frame, |
| const AtomicString& resource_ip_address) { |
| if (!frame) |
| return; |
| |
| // Just count these for the moment, don't block them. |
| if (network_utils::IsReservedIPAddress(resource_ip_address) && |
| frame->DomWindow()->AddressSpace() == |
| network::mojom::IPAddressSpace::kPublic) { |
| UseCounter::Count(frame->DomWindow(), |
| WebFeature::kMixedContentPrivateHostnameInPublicHostname); |
| // We can simplify the IP checks here, as we've already verified that |
| // |resourceIPAddress| is a reserved IP address, which means it's also a |
| // valid IP address in a normalized form. |
| if (resource_ip_address.StartsWith("127.0.0.") || |
| resource_ip_address == "[::1]") { |
| UseCounter::Count(frame->DomWindow(), |
| frame->DomWindow()->IsSecureContext() |
| ? WebFeature::kLoopbackEmbeddedInSecureContext |
| : WebFeature::kLoopbackEmbeddedInNonSecureContext); |
| } |
| } |
| } |
| |
| void MixedContentChecker::HandleCertificateError( |
| const ResourceResponse& response, |
| mojom::RequestContextType request_context, |
| bool strict_mixed_content_checking_for_plugin, |
| mojom::blink::ContentSecurityNotifier& notifier) { |
| WebMixedContentContextType context_type = |
| WebMixedContent::ContextTypeFromRequestContext( |
| request_context, strict_mixed_content_checking_for_plugin); |
| if (context_type == WebMixedContentContextType::kBlockable) { |
| notifier.NotifyContentWithCertificateErrorsRan(); |
| } else { |
| // contextTypeFromRequestContext() never returns NotMixedContent (it |
| // computes the type of mixed content, given that the content is mixed). |
| DCHECK_NE(context_type, WebMixedContentContextType::kNotMixedContent); |
| notifier.NotifyContentWithCertificateErrorsDisplayed(); |
| } |
| } |
| |
| // static |
| void MixedContentChecker::MixedContentFound( |
| LocalFrame* frame, |
| const KURL& main_resource_url, |
| const KURL& mixed_content_url, |
| mojom::RequestContextType request_context, |
| bool was_allowed, |
| const KURL& url_before_redirects, |
| bool had_redirect, |
| std::unique_ptr<SourceLocation> source_location) { |
| // Logs to the frame console. |
| frame->GetDocument()->AddConsoleMessage(CreateConsoleMessageAboutFetch( |
| main_resource_url, mixed_content_url, request_context, was_allowed, |
| std::move(source_location))); |
| |
| CreateMixedContentIssue( |
| main_resource_url, mixed_content_url, request_context, frame, |
| was_allowed |
| ? mojom::blink::MixedContentResolutionStatus::kMixedContentWarning |
| : mojom::blink::MixedContentResolutionStatus::kMixedContentBlocked, |
| base::Optional<String>()); |
| // Reports to the CSP policy. |
| ContentSecurityPolicy* policy = |
| frame->GetSecurityContext()->GetContentSecurityPolicy(); |
| if (policy) { |
| policy->ReportMixedContent( |
| url_before_redirects, |
| had_redirect ? ResourceRequest::RedirectStatus::kFollowedRedirect |
| : ResourceRequest::RedirectStatus::kNoRedirect); |
| } |
| } |
| |
| // static |
| ConsoleMessage* MixedContentChecker::CreateConsoleMessageAboutFetchAutoupgrade( |
| const KURL& main_resource_url, |
| const KURL& mixed_content_url) { |
| String message = String::Format( |
| "Mixed Content: The page at '%s' was loaded over HTTPS, but requested an " |
| "insecure element '%s'. This request was " |
| "automatically upgraded to HTTPS, For more information see " |
| "https://blog.chromium.org/2019/10/" |
| "no-more-mixed-messages-about-https.html", |
| main_resource_url.ElidedString().Utf8().c_str(), |
| mixed_content_url.ElidedString().Utf8().c_str()); |
| return MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kSecurity, |
| mojom::ConsoleMessageLevel::kWarning, message); |
| } |
| |
| WebMixedContentContextType MixedContentChecker::ContextTypeForInspector( |
| LocalFrame* frame, |
| const ResourceRequest& request) { |
| Frame* mixed_frame = InWhichFrameIsContentMixed(frame, request.Url()); |
| if (!mixed_frame) |
| return WebMixedContentContextType::kNotMixedContent; |
| |
| bool strict_mixed_content_checking_for_plugin = |
| mixed_frame->GetSettings() && |
| mixed_frame->GetSettings()->GetStrictMixedContentCheckingForPlugin(); |
| return WebMixedContent::ContextTypeFromRequestContext( |
| request.GetRequestContext(), strict_mixed_content_checking_for_plugin); |
| } |
| |
| // static |
| void MixedContentChecker::UpgradeInsecureRequest( |
| ResourceRequest& resource_request, |
| const FetchClientSettingsObject* fetch_client_settings_object, |
| ExecutionContext* execution_context_for_logging, |
| mojom::RequestContextFrameType frame_type, |
| WebContentSettingsClient* settings_client) { |
| // We always upgrade requests that meet any of the following criteria: |
| // 1. Are for subresources. |
| // 2. Are for nested frames. |
| // 3. Are form submissions. |
| // 4. Whose hosts are contained in the origin_context's upgrade insecure |
| // navigations set. |
| |
| // This happens for: |
| // * Browser initiated main document loading. No upgrade required. |
| // * Navigation initiated by a frame in another process. URL should have |
| // already been upgraded in the initiator's process. |
| if (!execution_context_for_logging) |
| return; |
| |
| DCHECK(fetch_client_settings_object); |
| |
| if ((fetch_client_settings_object->GetInsecureRequestsPolicy() & |
| mojom::blink::InsecureRequestPolicy::kUpgradeInsecureRequests) == |
| mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone) { |
| mojom::RequestContextType context = resource_request.GetRequestContext(); |
| if (context == mojom::RequestContextType::UNSPECIFIED || |
| !MixedContentChecker::ShouldAutoupgrade( |
| fetch_client_settings_object->GetHttpsState(), context, |
| settings_client, fetch_client_settings_object->GlobalObjectUrl())) { |
| return; |
| } |
| // We set the upgrade if insecure flag regardless of whether we autoupgrade |
| // due to scheme not being http, so any redirects get upgraded. |
| resource_request.SetUpgradeIfInsecure(true); |
| if (resource_request.Url().ProtocolIs("http")) { |
| if (auto* window = |
| DynamicTo<LocalDOMWindow>(execution_context_for_logging)) { |
| window->AddConsoleMessage( |
| MixedContentChecker::CreateConsoleMessageAboutFetchAutoupgrade( |
| fetch_client_settings_object->GlobalObjectUrl(), |
| resource_request.Url())); |
| resource_request.SetUkmSourceId(window->document()->UkmSourceID()); |
| CreateMixedContentIssue(fetch_client_settings_object->GlobalObjectUrl(), |
| resource_request.Url(), context, |
| window->document()->GetFrame(), |
| mojom::blink::MixedContentResolutionStatus:: |
| kMixedContentAutomaticallyUpgraded, |
| resource_request.GetDevToolsId()); |
| } |
| resource_request.SetIsAutomaticUpgrade(true); |
| } else { |
| return; |
| } |
| } |
| |
| // Nested frames are always upgraded on the browser process. |
| if (frame_type == mojom::RequestContextFrameType::kNested) |
| return; |
| |
| // We set the UpgradeIfInsecure flag even if the current request wasn't |
| // upgraded (due to already being HTTPS), since we still need to upgrade |
| // redirects if they are not to HTTPS URLs. |
| resource_request.SetUpgradeIfInsecure(true); |
| |
| KURL url = resource_request.Url(); |
| |
| if (!url.ProtocolIs("http") || |
| SecurityOrigin::Create(url)->IsPotentiallyTrustworthy()) { |
| return; |
| } |
| |
| if (frame_type == mojom::RequestContextFrameType::kNone || |
| resource_request.GetRequestContext() == mojom::RequestContextType::FORM || |
| (!url.Host().IsNull() && |
| fetch_client_settings_object->GetUpgradeInsecureNavigationsSet() |
| .Contains(url.Host().Impl()->GetHash()))) { |
| if (!resource_request.IsAutomaticUpgrade()) { |
| // These UseCounters are specific for UpgradeInsecureRequests, don't log |
| // for autoupgrades. |
| mojom::RequestContextType context = resource_request.GetRequestContext(); |
| if (context == mojom::RequestContextType::UNSPECIFIED) { |
| UseCounter::Count( |
| execution_context_for_logging, |
| WebFeature::kUpgradeInsecureRequestsUpgradedRequestUnknown); |
| } else { |
| WebMixedContentContextType content_type = |
| WebMixedContent::ContextTypeFromRequestContext(context, false); |
| switch (content_type) { |
| case WebMixedContentContextType::kOptionallyBlockable: |
| UseCounter::Count( |
| execution_context_for_logging, |
| WebFeature:: |
| kUpgradeInsecureRequestsUpgradedRequestOptionallyBlockable); |
| break; |
| case WebMixedContentContextType::kBlockable: |
| case WebMixedContentContextType::kShouldBeBlockable: |
| UseCounter::Count( |
| execution_context_for_logging, |
| WebFeature::kUpgradeInsecureRequestsUpgradedRequestBlockable); |
| break; |
| case WebMixedContentContextType::kNotMixedContent: |
| NOTREACHED(); |
| } |
| } |
| } |
| url.SetProtocol("https"); |
| if (url.Port() == 80) |
| url.SetPort(443); |
| resource_request.SetUrl(url); |
| } |
| } |
| |
| } // namespace blink |