| 1 | /* |
| 2 | * Copyright (C) 2006-2018 Apple Inc. All rights reserved. |
| 3 | * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| 4 | * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/) |
| 5 | * |
| 6 | * Redistribution and use in source and binary forms, with or without |
| 7 | * modification, are permitted provided that the following conditions |
| 8 | * are met: |
| 9 | * |
| 10 | * 1. Redistributions of source code must retain the above copyright |
| 11 | * notice, this list of conditions and the following disclaimer. |
| 12 | * 2. Redistributions in binary form must reproduce the above copyright |
| 13 | * notice, this list of conditions and the following disclaimer in the |
| 14 | * documentation and/or other materials provided with the distribution. |
| 15 | * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| 16 | * its contributors may be used to endorse or promote products derived |
| 17 | * from this software without specific prior written permission. |
| 18 | * |
| 19 | * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| 20 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 22 | * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 23 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| 26 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| 28 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 | */ |
| 30 | |
| 31 | #include "config.h" |
| 32 | #include "PolicyChecker.h" |
| 33 | |
| 34 | #include "BlobRegistry.h" |
| 35 | #include "BlobURL.h" |
| 36 | #include "ContentFilter.h" |
| 37 | #include "ContentSecurityPolicy.h" |
| 38 | #include "DOMWindow.h" |
| 39 | #include "DocumentLoader.h" |
| 40 | #include "Event.h" |
| 41 | #include "EventNames.h" |
| 42 | #include "FormState.h" |
| 43 | #include "Frame.h" |
| 44 | #include "FrameLoader.h" |
| 45 | #include "FrameLoaderClient.h" |
| 46 | #include "HTMLFormElement.h" |
| 47 | #include "HTMLFrameOwnerElement.h" |
| 48 | #include "HTMLPlugInElement.h" |
| 49 | #include "Logging.h" |
| 50 | #include <wtf/CompletionHandler.h> |
| 51 | |
| 52 | #if USE(QUICK_LOOK) |
| 53 | #include "QuickLook.h" |
| 54 | #endif |
| 55 | |
| 56 | namespace WebCore { |
| 57 | |
| 58 | static bool isAllowedByContentSecurityPolicy(const URL& url, const Element* ownerElement, bool didReceiveRedirectResponse) |
| 59 | { |
| 60 | if (!ownerElement) |
| 61 | return true; |
| 62 | // Elements in user agent show tree should load whatever the embedding document policy is. |
| 63 | if (ownerElement->isInUserAgentShadowTree()) |
| 64 | return true; |
| 65 | |
| 66 | auto redirectResponseReceived = didReceiveRedirectResponse ? ContentSecurityPolicy::RedirectResponseReceived::Yes : ContentSecurityPolicy::RedirectResponseReceived::No; |
| 67 | |
| 68 | ASSERT(ownerElement->document().contentSecurityPolicy()); |
| 69 | if (is<HTMLPlugInElement>(ownerElement)) |
| 70 | return ownerElement->document().contentSecurityPolicy()->allowObjectFromSource(url, redirectResponseReceived); |
| 71 | return ownerElement->document().contentSecurityPolicy()->allowChildFrameFromSource(url, redirectResponseReceived); |
| 72 | } |
| 73 | |
| 74 | PolicyCheckIdentifier PolicyCheckIdentifier::create() |
| 75 | { |
| 76 | static uint64_t identifier = 0; |
| 77 | identifier++; |
| 78 | return PolicyCheckIdentifier { Process::identifier(), identifier }; |
| 79 | } |
| 80 | |
| 81 | bool PolicyCheckIdentifier::isValidFor(PolicyCheckIdentifier expectedIdentifier) |
| 82 | { |
| 83 | RELEASE_ASSERT_WITH_MESSAGE(m_policyCheck, "Received 0 as the policy check identifier" ); |
| 84 | RELEASE_ASSERT_WITH_MESSAGE(m_process == expectedIdentifier.m_process, "Received a policy check response for a wrong process" ); |
| 85 | RELEASE_ASSERT_WITH_MESSAGE(m_policyCheck <= expectedIdentifier.m_policyCheck, "Received a policy check response from the future" ); |
| 86 | return m_policyCheck == expectedIdentifier.m_policyCheck; |
| 87 | } |
| 88 | |
| 89 | PolicyChecker::PolicyChecker(Frame& frame) |
| 90 | : m_frame(frame) |
| 91 | , m_delegateIsDecidingNavigationPolicy(false) |
| 92 | , m_delegateIsHandlingUnimplementablePolicy(false) |
| 93 | , m_loadType(FrameLoadType::Standard) |
| 94 | { |
| 95 | } |
| 96 | |
| 97 | void PolicyChecker::checkNavigationPolicy(ResourceRequest&& newRequest, const ResourceResponse& redirectResponse, NavigationPolicyDecisionFunction&& function) |
| 98 | { |
| 99 | checkNavigationPolicy(WTFMove(newRequest), redirectResponse, m_frame.loader().activeDocumentLoader(), { }, WTFMove(function)); |
| 100 | } |
| 101 | |
| 102 | CompletionHandlerCallingScope PolicyChecker::extendBlobURLLifetimeIfNecessary(ResourceRequest& request) const |
| 103 | { |
| 104 | if (!request.url().protocolIsBlob()) |
| 105 | return { }; |
| 106 | |
| 107 | // Create a new temporary blobURL in case this one gets revoked during the asynchronous navigation policy decision. |
| 108 | URL temporaryBlobURL = BlobURL::createPublicURL(&m_frame.document()->securityOrigin()); |
| 109 | blobRegistry().registerBlobURL(temporaryBlobURL, request.url()); |
| 110 | request.setURL(temporaryBlobURL); |
| 111 | return CompletionHandler<void()>([temporaryBlobURL = WTFMove(temporaryBlobURL)] { |
| 112 | blobRegistry().unregisterBlobURL(temporaryBlobURL); |
| 113 | }); |
| 114 | } |
| 115 | |
| 116 | void PolicyChecker::checkNavigationPolicy(ResourceRequest&& request, const ResourceResponse& redirectResponse, DocumentLoader* loader, RefPtr<FormState>&& formState, NavigationPolicyDecisionFunction&& function, PolicyDecisionMode policyDecisionMode) |
| 117 | { |
| 118 | NavigationAction action = loader->triggeringAction(); |
| 119 | if (action.isEmpty()) { |
| 120 | action = NavigationAction { *m_frame.document(), request, InitiatedByMainFrame::Unknown, NavigationType::Other, loader->shouldOpenExternalURLsPolicyToPropagate() }; |
| 121 | loader->setTriggeringAction(NavigationAction { action }); |
| 122 | } |
| 123 | |
| 124 | if (m_frame.page() && m_frame.page()->openedByDOMWithOpener()) |
| 125 | action.setOpenedByDOMWithOpener(); |
| 126 | action.setHasOpenedFrames(m_frame.loader().hasOpenedFrames()); |
| 127 | |
| 128 | // Don't ask more than once for the same request or if we are loading an empty URL. |
| 129 | // This avoids confusion on the part of the client. |
| 130 | if (equalIgnoringHeaderFields(request, loader->lastCheckedRequest()) || (!request.isNull() && request.url().isEmpty())) { |
| 131 | function(ResourceRequest(request), { }, NavigationPolicyDecision::ContinueLoad); |
| 132 | loader->setLastCheckedRequest(WTFMove(request)); |
| 133 | return; |
| 134 | } |
| 135 | |
| 136 | // We are always willing to show alternate content for unreachable URLs; |
| 137 | // treat it like a reload so it maintains the right state for b/f list. |
| 138 | auto& substituteData = loader->substituteData(); |
| 139 | if (substituteData.isValid() && !substituteData.failingURL().isEmpty()) { |
| 140 | bool shouldContinue = true; |
| 141 | #if ENABLE(CONTENT_FILTERING) |
| 142 | shouldContinue = ContentFilter::continueAfterSubstituteDataRequest(*m_frame.loader().activeDocumentLoader(), substituteData); |
| 143 | #endif |
| 144 | if (isBackForwardLoadType(m_loadType)) |
| 145 | m_loadType = FrameLoadType::Reload; |
| 146 | function(WTFMove(request), { }, shouldContinue ? NavigationPolicyDecision::ContinueLoad : NavigationPolicyDecision::IgnoreLoad); |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | if (!isAllowedByContentSecurityPolicy(request.url(), m_frame.ownerElement(), !redirectResponse.isNull())) { |
| 151 | if (m_frame.ownerElement()) { |
| 152 | // Fire a load event (even though we were blocked by CSP) as timing attacks would otherwise |
| 153 | // reveal that the frame was blocked. This way, it looks like any other cross-origin page load. |
| 154 | m_frame.ownerElement()->dispatchEvent(Event::create(eventNames().loadEvent, Event::CanBubble::No, Event::IsCancelable::No)); |
| 155 | } |
| 156 | function(WTFMove(request), { }, NavigationPolicyDecision::IgnoreLoad); |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | loader->setLastCheckedRequest(ResourceRequest(request)); |
| 161 | |
| 162 | // Initial 'about:blank' load needs to happen synchronously so the policy check needs to be synchronous in this case. |
| 163 | if (!m_frame.loader().stateMachine().committedFirstRealDocumentLoad() && request.url().protocolIsAbout() && !substituteData.isValid()) |
| 164 | policyDecisionMode = PolicyDecisionMode::Synchronous; |
| 165 | |
| 166 | #if USE(QUICK_LOOK) |
| 167 | // Always allow QuickLook-generated URLs based on the protocol scheme. |
| 168 | if (!request.isNull() && isQuickLookPreviewURL(request.url())) |
| 169 | return function(WTFMove(request), makeWeakPtr(formState.get()), NavigationPolicyDecision::ContinueLoad); |
| 170 | #endif |
| 171 | |
| 172 | #if ENABLE(CONTENT_FILTERING) |
| 173 | if (m_contentFilterUnblockHandler.canHandleRequest(request)) { |
| 174 | RefPtr<Frame> frame { &m_frame }; |
| 175 | m_contentFilterUnblockHandler.requestUnblockAsync([frame](bool unblocked) { |
| 176 | if (unblocked) |
| 177 | frame->loader().reload(); |
| 178 | }); |
| 179 | return function({ }, nullptr, NavigationPolicyDecision::IgnoreLoad); |
| 180 | } |
| 181 | m_contentFilterUnblockHandler = { }; |
| 182 | #endif |
| 183 | |
| 184 | m_frame.loader().clearProvisionalLoadForPolicyCheck(); |
| 185 | |
| 186 | auto blobURLLifetimeExtension = policyDecisionMode == PolicyDecisionMode::Asynchronous ? extendBlobURLLifetimeIfNecessary(request) : CompletionHandlerCallingScope { }; |
| 187 | |
| 188 | auto requestIdentifier = PolicyCheckIdentifier::create(); |
| 189 | m_delegateIsDecidingNavigationPolicy = true; |
| 190 | String suggestedFilename = action.downloadAttribute().isEmpty() ? nullAtom() : action.downloadAttribute(); |
| 191 | m_frame.loader().client().dispatchDecidePolicyForNavigationAction(action, request, redirectResponse, formState.get(), policyDecisionMode, requestIdentifier, |
| 192 | [this, function = WTFMove(function), request = ResourceRequest(request), formState = WTFMove(formState), suggestedFilename = WTFMove(suggestedFilename), |
| 193 | blobURLLifetimeExtension = WTFMove(blobURLLifetimeExtension), requestIdentifier] (PolicyAction policyAction, PolicyCheckIdentifier responseIdentifier) mutable { |
| 194 | |
| 195 | if (!responseIdentifier.isValidFor(requestIdentifier)) |
| 196 | return function({ }, nullptr, NavigationPolicyDecision::IgnoreLoad); |
| 197 | |
| 198 | m_delegateIsDecidingNavigationPolicy = false; |
| 199 | |
| 200 | switch (policyAction) { |
| 201 | case PolicyAction::Download: |
| 202 | m_frame.loader().setOriginalURLForDownloadRequest(request); |
| 203 | m_frame.loader().client().startDownload(request, suggestedFilename); |
| 204 | FALLTHROUGH; |
| 205 | case PolicyAction::Ignore: |
| 206 | return function({ }, nullptr, NavigationPolicyDecision::IgnoreLoad); |
| 207 | case PolicyAction::StopAllLoads: |
| 208 | function({ }, nullptr, NavigationPolicyDecision::StopAllLoads); |
| 209 | return; |
| 210 | case PolicyAction::Use: |
| 211 | if (!m_frame.loader().client().canHandleRequest(request)) { |
| 212 | handleUnimplementablePolicy(m_frame.loader().client().cannotShowURLError(request)); |
| 213 | return function({ }, { }, NavigationPolicyDecision::IgnoreLoad); |
| 214 | } |
| 215 | return function(WTFMove(request), makeWeakPtr(formState.get()), NavigationPolicyDecision::ContinueLoad); |
| 216 | } |
| 217 | ASSERT_NOT_REACHED(); |
| 218 | }); |
| 219 | } |
| 220 | |
| 221 | void PolicyChecker::checkNewWindowPolicy(NavigationAction&& navigationAction, ResourceRequest&& request, RefPtr<FormState>&& formState, const String& frameName, NewWindowPolicyDecisionFunction&& function) |
| 222 | { |
| 223 | if (m_frame.document() && m_frame.document()->isSandboxed(SandboxPopups)) |
| 224 | return function({ }, nullptr, { }, { }, ShouldContinue::No); |
| 225 | |
| 226 | if (!DOMWindow::allowPopUp(m_frame)) |
| 227 | return function({ }, nullptr, { }, { }, ShouldContinue::No); |
| 228 | |
| 229 | auto blobURLLifetimeExtension = extendBlobURLLifetimeIfNecessary(request); |
| 230 | |
| 231 | auto requestIdentifier = PolicyCheckIdentifier::create(); |
| 232 | m_frame.loader().client().dispatchDecidePolicyForNewWindowAction(navigationAction, request, formState.get(), frameName, requestIdentifier, [frame = makeRef(m_frame), request, |
| 233 | formState = WTFMove(formState), frameName, navigationAction, function = WTFMove(function), blobURLLifetimeExtension = WTFMove(blobURLLifetimeExtension), |
| 234 | requestIdentifier] (PolicyAction policyAction, PolicyCheckIdentifier responseIdentifier) mutable { |
| 235 | |
| 236 | if (!responseIdentifier.isValidFor(requestIdentifier)) |
| 237 | return function({ }, nullptr, { }, { }, ShouldContinue::No); |
| 238 | |
| 239 | switch (policyAction) { |
| 240 | case PolicyAction::Download: |
| 241 | frame->loader().client().startDownload(request); |
| 242 | FALLTHROUGH; |
| 243 | case PolicyAction::Ignore: |
| 244 | function({ }, nullptr, { }, { }, ShouldContinue::No); |
| 245 | return; |
| 246 | case PolicyAction::StopAllLoads: |
| 247 | ASSERT_NOT_REACHED(); |
| 248 | function({ }, nullptr, { }, { }, ShouldContinue::No); |
| 249 | return; |
| 250 | case PolicyAction::Use: |
| 251 | function(request, makeWeakPtr(formState.get()), frameName, navigationAction, ShouldContinue::Yes); |
| 252 | return; |
| 253 | } |
| 254 | ASSERT_NOT_REACHED(); |
| 255 | }); |
| 256 | } |
| 257 | |
| 258 | void PolicyChecker::stopCheck() |
| 259 | { |
| 260 | m_frame.loader().client().cancelPolicyCheck(); |
| 261 | } |
| 262 | |
| 263 | void PolicyChecker::cannotShowMIMEType(const ResourceResponse& response) |
| 264 | { |
| 265 | handleUnimplementablePolicy(m_frame.loader().client().cannotShowMIMETypeError(response)); |
| 266 | } |
| 267 | |
| 268 | void PolicyChecker::handleUnimplementablePolicy(const ResourceError& error) |
| 269 | { |
| 270 | m_delegateIsHandlingUnimplementablePolicy = true; |
| 271 | m_frame.loader().client().dispatchUnableToImplementPolicy(error); |
| 272 | m_delegateIsHandlingUnimplementablePolicy = false; |
| 273 | } |
| 274 | |
| 275 | } // namespace WebCore |
| 276 | |