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