| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/script/document_write_intervention.h" |
| |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h" |
| #include "third_party/blink/public/platform/web_effective_connection_type.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/resource/script_resource.h" |
| #include "third_party/blink/renderer/core/probe/core_probes.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/script_fetch_options.h" |
| #include "third_party/blink/renderer/platform/network/network_state_notifier.h" |
| #include "third_party/blink/renderer/platform/network/network_utils.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| void EmitWarningMayBeBlocked(const String& url, Document& document) { |
| String message = |
| "A parser-blocking, cross site (i.e. different eTLD+1) script, " + url + |
| ", is invoked via document.write. The network request for this script " |
| "MAY be blocked by the browser in this or a future page load due to poor " |
| "network connectivity. If blocked in this page load, it will be " |
| "confirmed in a subsequent console message. " |
| "See https://www.chromestatus.com/feature/5718547946799104 " |
| "for more details."; |
| document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kJavaScript, |
| mojom::ConsoleMessageLevel::kWarning, message)); |
| DVLOG(1) << message.Utf8(); |
| } |
| |
| void EmitWarningNotBlocked(const String& url, Document& document) { |
| String message = |
| "The parser-blocking, cross site (i.e. different eTLD+1) script, " + url + |
| ", invoked via document.write was NOT BLOCKED on this page load, but MAY " |
| "be blocked by the browser in future page loads with poor network " |
| "connectivity."; |
| document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kJavaScript, |
| mojom::ConsoleMessageLevel::kWarning, message)); |
| } |
| |
| void EmitErrorBlocked(const String& url, Document& document) { |
| String message = |
| "Network request for the parser-blocking, cross site (i.e. different " |
| "eTLD+1) script, " + |
| url + |
| ", invoked via document.write was BLOCKED by the browser due to poor " |
| "network connectivity. "; |
| document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kIntervention, |
| mojom::ConsoleMessageLevel::kError, message)); |
| } |
| |
| void AddWarningHeader(FetchParameters* params) { |
| params->MutableResourceRequest().AddHttpHeaderField( |
| "Intervention", |
| "<https://www.chromestatus.com/feature/5718547946799104>; " |
| "level=\"warning\""); |
| } |
| |
| void AddHeader(FetchParameters* params) { |
| params->MutableResourceRequest().AddHttpHeaderField( |
| "Intervention", |
| "<https://www.chromestatus.com/feature/5718547946799104>"); |
| } |
| |
| bool IsConnectionEffectively2G(WebEffectiveConnectionType effective_type) { |
| switch (effective_type) { |
| case WebEffectiveConnectionType::kTypeSlow2G: |
| case WebEffectiveConnectionType::kType2G: |
| return true; |
| case WebEffectiveConnectionType::kType3G: |
| case WebEffectiveConnectionType::kType4G: |
| case WebEffectiveConnectionType::kTypeUnknown: |
| case WebEffectiveConnectionType::kTypeOffline: |
| return false; |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| bool ShouldDisallowFetch(Settings* settings, |
| WebConnectionType connection_type, |
| WebEffectiveConnectionType effective_connection) { |
| if (settings->GetDisallowFetchForDocWrittenScriptsInMainFrame()) |
| return true; |
| if (settings |
| ->GetDisallowFetchForDocWrittenScriptsInMainFrameOnSlowConnections() && |
| connection_type == kWebConnectionTypeCellular2G) |
| return true; |
| if (settings |
| ->GetDisallowFetchForDocWrittenScriptsInMainFrameIfEffectively2G() && |
| IsConnectionEffectively2G(effective_connection)) |
| return true; |
| return false; |
| } |
| |
| } // namespace |
| |
| bool MaybeDisallowFetchForDocWrittenScript(FetchParameters& params, |
| Document& document) { |
| // Only scripts inserted via document.write are candidates for having their |
| // fetch disallowed. |
| if (!document.IsInDocumentWrite()) |
| return false; |
| |
| Settings* settings = document.GetSettings(); |
| if (!settings) |
| return false; |
| |
| if (!document.GetFrame() || !document.GetFrame()->IsMainFrame()) |
| return false; |
| |
| // Only block synchronously loaded (parser blocking) scripts. |
| if (params.Defer() != FetchParameters::kNoDefer) |
| return false; |
| |
| probe::DocumentWriteFetchScript(&document); |
| |
| if (!params.Url().ProtocolIsInHTTPFamily()) |
| return false; |
| |
| // Avoid blocking same origin scripts, as they may be used to render main |
| // page content, whereas cross-origin scripts inserted via document.write |
| // are likely to be third party content. |
| String request_host = params.Url().Host(); |
| String document_host = document.domWindow()->GetSecurityOrigin()->Domain(); |
| |
| bool same_site = false; |
| if (request_host == document_host) |
| same_site = true; |
| |
| // If the hosts didn't match, then see if the domains match. For example, if |
| // a script is served from static.example.com for a document served from |
| // www.example.com, we consider that a first party script and allow it. |
| String request_domain = network_utils::GetDomainAndRegistry( |
| request_host, network_utils::kIncludePrivateRegistries); |
| String document_domain = network_utils::GetDomainAndRegistry( |
| document_host, network_utils::kIncludePrivateRegistries); |
| // getDomainAndRegistry will return the empty string for domains that are |
| // already top-level, such as localhost. Thus we only compare domains if we |
| // get non-empty results back from getDomainAndRegistry. |
| if (!request_domain.IsEmpty() && !document_domain.IsEmpty() && |
| request_domain == document_domain) |
| same_site = true; |
| |
| if (same_site) { |
| // This histogram is introduced to help decide whether we should also check |
| // same scheme while deciding whether or not to block the script as is done |
| // in other cases of "same site" usage. On the other hand we do not want to |
| // block more scripts than necessary. |
| if (params.Url().Protocol() != |
| document.domWindow()->GetSecurityOrigin()->Protocol()) { |
| document.Loader()->DidObserveLoadingBehavior( |
| LoadingBehaviorFlag:: |
| kLoadingBehaviorDocumentWriteBlockDifferentScheme); |
| } |
| return false; |
| } |
| |
| EmitWarningMayBeBlocked(params.Url().GetString(), document); |
| |
| // Do not block scripts if it is a page reload. This is to enable pages to |
| // recover if blocking of a script is leading to a page break and the user |
| // reloads the page. |
| const WebFrameLoadType load_type = document.Loader()->LoadType(); |
| if (IsReloadLoadType(load_type)) { |
| // Recording this metric since an increase in number of reloads for pages |
| // where a script was blocked could be indicative of a page break. |
| document.Loader()->DidObserveLoadingBehavior( |
| LoadingBehaviorFlag::kLoadingBehaviorDocumentWriteBlockReload); |
| AddWarningHeader(¶ms); |
| return false; |
| } |
| |
| // Add the metadata that this page has scripts inserted via document.write |
| // that are eligible for blocking. Note that if there are multiple scripts |
| // the flag will be conveyed to the browser process only once. |
| document.Loader()->DidObserveLoadingBehavior( |
| LoadingBehaviorFlag::kLoadingBehaviorDocumentWriteBlock); |
| |
| if (!ShouldDisallowFetch(settings, GetNetworkStateNotifier().ConnectionType(), |
| GetNetworkStateNotifier().EffectiveType())) { |
| AddWarningHeader(¶ms); |
| return false; |
| } |
| |
| AddWarningHeader(¶ms); |
| |
| params.MutableResourceRequest().SetCacheMode( |
| mojom::FetchCacheMode::kOnlyIfCached); |
| |
| return true; |
| } |
| |
| void PossiblyFetchBlockedDocWriteScript( |
| const Resource* resource, |
| Document& element_document, |
| const ScriptFetchOptions& options, |
| CrossOriginAttributeValue cross_origin) { |
| if (!resource->ErrorOccurred()) { |
| EmitWarningNotBlocked(resource->Url(), element_document); |
| return; |
| } |
| |
| // Due to dependency violation, not able to check the exact error to be |
| // ERR_CACHE_MISS but other errors are rare with |
| // mojom::FetchCacheMode::kOnlyIfCached. |
| |
| EmitErrorBlocked(resource->Url(), element_document); |
| |
| FetchParameters params(options.CreateFetchParameters( |
| resource->Url(), |
| element_document.GetExecutionContext()->GetSecurityOrigin(), cross_origin, |
| resource->Encoding(), FetchParameters::kIdleLoad)); |
| AddHeader(¶ms); |
| ScriptResource::Fetch(params, element_document.Fetcher(), nullptr, |
| ScriptResource::kNoStreaming); |
| } |
| |
| } // namespace blink |