1/*
2 * Copyright (C) 2018-2019 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "NetworkLoadChecker.h"
28
29#include "Download.h"
30#include "Logging.h"
31#include "NetworkCORSPreflightChecker.h"
32#include "NetworkProcess.h"
33#include <WebCore/ContentRuleListResults.h>
34#include <WebCore/ContentSecurityPolicy.h>
35#include <WebCore/CrossOriginAccessControl.h>
36#include <WebCore/CrossOriginPreflightResultCache.h>
37#include <WebCore/SchemeRegistry.h>
38#include <wtf/Scope.h>
39
40#define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(m_sessionID.isAlwaysOnLoggingAllowed(), Network, "%p - NetworkLoadChecker::" fmt, this, ##__VA_ARGS__)
41
42namespace WebKit {
43
44using namespace WebCore;
45
46static inline bool isSameOrigin(const URL& url, const SecurityOrigin* origin)
47{
48 return url.protocolIsData() || url.protocolIsBlob() || !origin || origin->canRequest(url);
49}
50
51NetworkLoadChecker::NetworkLoadChecker(NetworkProcess& networkProcess, FetchOptions&& options, PAL::SessionID sessionID, PageIdentifier pageID, uint64_t frameID, HTTPHeaderMap&& originalRequestHeaders, URL&& url, RefPtr<SecurityOrigin>&& sourceOrigin, PreflightPolicy preflightPolicy, String&& referrer, bool isHTTPSUpgradeEnabled, bool shouldCaptureExtraNetworkLoadMetrics, LoadType requestLoadType)
52 : m_options(WTFMove(options))
53 , m_sessionID(sessionID)
54 , m_networkProcess(networkProcess)
55 , m_pageID(pageID)
56 , m_frameID(frameID)
57 , m_originalRequestHeaders(WTFMove(originalRequestHeaders))
58 , m_url(WTFMove(url))
59 , m_origin(WTFMove(sourceOrigin))
60 , m_preflightPolicy(preflightPolicy)
61 , m_referrer(WTFMove(referrer))
62 , m_shouldCaptureExtraNetworkLoadMetrics(shouldCaptureExtraNetworkLoadMetrics)
63 , m_isHTTPSUpgradeEnabled(isHTTPSUpgradeEnabled)
64 , m_requestLoadType(requestLoadType)
65{
66 m_isSameOriginRequest = isSameOrigin(m_url, m_origin.get());
67 switch (options.credentials) {
68 case FetchOptions::Credentials::Include:
69 m_storedCredentialsPolicy = StoredCredentialsPolicy::Use;
70 break;
71 case FetchOptions::Credentials::SameOrigin:
72 m_storedCredentialsPolicy = m_isSameOriginRequest ? StoredCredentialsPolicy::Use : StoredCredentialsPolicy::DoNotUse;
73 break;
74 case FetchOptions::Credentials::Omit:
75 m_storedCredentialsPolicy = StoredCredentialsPolicy::DoNotUse;
76 break;
77 }
78}
79
80NetworkLoadChecker::~NetworkLoadChecker() = default;
81
82void NetworkLoadChecker::check(ResourceRequest&& request, ContentSecurityPolicyClient* client, ValidationHandler&& handler)
83{
84 ASSERT(!isChecking());
85
86 if (m_shouldCaptureExtraNetworkLoadMetrics)
87 m_loadInformation.request = request;
88
89 m_firstRequestHeaders = request.httpHeaderFields();
90 checkRequest(WTFMove(request), client, WTFMove(handler));
91}
92
93static inline NetworkLoadChecker::RedirectionRequestOrError redirectionError(const ResourceResponse& redirectResponse, String&& errorMessage)
94{
95 return makeUnexpected(ResourceError { String { }, 0, redirectResponse.url(), WTFMove(errorMessage), ResourceError::Type::AccessControl });
96}
97
98void NetworkLoadChecker::checkRedirection(ResourceRequest&& request, ResourceRequest&& redirectRequest, ResourceResponse&& redirectResponse, ContentSecurityPolicyClient* client, RedirectionValidationHandler&& handler)
99{
100 ASSERT(!isChecking());
101
102 auto error = validateResponse(redirectResponse);
103 if (!error.isNull()) {
104 handler(redirectionError(redirectResponse, makeString("Cross-origin redirection to ", redirectRequest.url().string(), " denied by Cross-Origin Resource Sharing policy: ", error.localizedDescription())));
105 return;
106 }
107
108 if (m_options.redirect == FetchOptions::Redirect::Error) {
109 handler(redirectionError(redirectResponse, makeString("Not allowed to follow a redirection while loading ", redirectResponse.url().string())));
110 return;
111 }
112 if (m_options.redirect == FetchOptions::Redirect::Manual) {
113 handler(RedirectionTriplet { WTFMove(request), WTFMove(redirectRequest), WTFMove(redirectResponse) });
114 return;
115 }
116
117 // FIXME: We should check that redirections are only HTTP(s) as per fetch spec.
118 // See https://github.com/whatwg/fetch/issues/393
119
120 if (++m_redirectCount > 20) {
121 handler(redirectionError(redirectResponse, "Load cannot follow more than 20 redirections"_s));
122 return;
123 }
124
125 m_previousURL = WTFMove(m_url);
126 m_url = redirectRequest.url();
127
128 checkRequest(WTFMove(redirectRequest), client, [handler = WTFMove(handler), request = WTFMove(request), redirectResponse = WTFMove(redirectResponse)](auto&& result) mutable {
129 WTF::switchOn(result,
130 [&handler] (ResourceError& error) mutable {
131 handler(makeUnexpected(WTFMove(error)));
132 },
133 [&handler, &request, &redirectResponse] (RedirectionTriplet& triplet) mutable {
134 // FIXME: if checkRequest returns a RedirectionTriplet, it means the requested URL has changed and we should update the redirectResponse to match.
135 handler(RedirectionTriplet { WTFMove(request), WTFMove(triplet.redirectRequest), WTFMove(redirectResponse) });
136 },
137 [&handler, &request, &redirectResponse] (ResourceRequest& redirectRequest) mutable {
138 handler(RedirectionTriplet { WTFMove(request), WTFMove(redirectRequest), WTFMove(redirectResponse) });
139 }
140 );
141 });
142}
143
144ResourceError NetworkLoadChecker::validateResponse(ResourceResponse& response)
145{
146 if (m_redirectCount)
147 response.setRedirected(true);
148
149 if (response.type() == ResourceResponse::Type::Opaqueredirect) {
150 response.setTainting(ResourceResponse::Tainting::Opaqueredirect);
151 return { };
152 }
153
154 if (m_options.mode == FetchOptions::Mode::Navigate || m_isSameOriginRequest) {
155 response.setTainting(ResourceResponse::Tainting::Basic);
156 return { };
157 }
158
159 if (m_options.mode == FetchOptions::Mode::NoCors) {
160 if (auto error = validateCrossOriginResourcePolicy(*m_origin, m_url, response))
161 return WTFMove(*error);
162
163 response.setTainting(ResourceResponse::Tainting::Opaque);
164 return { };
165 }
166
167 ASSERT(m_options.mode == FetchOptions::Mode::Cors);
168
169 // If we have a 304, the cached response is in WebProcess so we let WebProcess do the CORS check on the cached response.
170 if (response.httpStatusCode() == 304)
171 return { };
172
173 String errorMessage;
174 if (!passesAccessControlCheck(response, m_storedCredentialsPolicy, *m_origin, errorMessage))
175 return ResourceError { String { }, 0, m_url, WTFMove(errorMessage), ResourceError::Type::AccessControl };
176
177 response.setTainting(ResourceResponse::Tainting::Cors);
178 return { };
179}
180
181auto NetworkLoadChecker::accessControlErrorForValidationHandler(String&& message) -> RequestOrRedirectionTripletOrError
182{
183 return ResourceError { String { }, 0, m_url, WTFMove(message), ResourceError::Type::AccessControl };
184}
185
186void NetworkLoadChecker::applyHTTPSUpgradeIfNeeded(ResourceRequest&& request, CompletionHandler<void(ResourceRequest&&)>&& handler) const
187{
188#if PLATFORM(COCOA)
189 if (!m_isHTTPSUpgradeEnabled || m_requestLoadType != LoadType::MainFrame) {
190 handler(WTFMove(request));
191 return;
192 }
193
194 auto& url = request.url();
195
196 // Only upgrade http urls.
197 if (!url.protocolIs("http")) {
198 handler(WTFMove(request));
199 return;
200 }
201
202 auto& httpsUpgradeChecker = m_networkProcess->networkHTTPSUpgradeChecker();
203
204 // Do not wait for httpsUpgradeChecker to complete its setup.
205 if (!httpsUpgradeChecker.didSetupCompleteSuccessfully()) {
206 handler(WTFMove(request));
207 return;
208 }
209
210 httpsUpgradeChecker.query(url.host().toString(), m_sessionID, [request = WTFMove(request), handler = WTFMove(handler)] (bool foundHost) mutable {
211 if (foundHost) {
212 auto newURL = request.url();
213 newURL.setProtocol("https"_s);
214 request.setURL(newURL);
215 }
216
217 handler(WTFMove(request));
218 });
219#else
220 handler(WTFMove(request));
221#endif
222}
223
224void NetworkLoadChecker::checkRequest(ResourceRequest&& request, ContentSecurityPolicyClient* client, ValidationHandler&& handler)
225{
226 ResourceRequest originalRequest = request;
227
228 applyHTTPSUpgradeIfNeeded(WTFMove(request), [this, weakThis = makeWeakPtr(*this), client, handler = WTFMove(handler), originalRequest = WTFMove(originalRequest)](auto request) mutable {
229 if (!weakThis)
230 return handler({ ResourceError { ResourceError::Type::Cancellation }});
231
232 if (auto* contentSecurityPolicy = this->contentSecurityPolicy()) {
233 if (this->isRedirected()) {
234 auto type = m_options.mode == FetchOptions::Mode::Navigate ? ContentSecurityPolicy::InsecureRequestType::Navigation : ContentSecurityPolicy::InsecureRequestType::Load;
235 contentSecurityPolicy->upgradeInsecureRequestIfNeeded(request, type);
236 }
237 if (!this->isAllowedByContentSecurityPolicy(request, client)) {
238 handler(this->accessControlErrorForValidationHandler("Blocked by Content Security Policy."_s));
239 return;
240 }
241 }
242
243#if ENABLE(CONTENT_EXTENSIONS)
244 this->processContentRuleListsForLoad(WTFMove(request), [this, weakThis = WTFMove(weakThis), handler = WTFMove(handler), originalRequest = WTFMove(originalRequest)](auto result) mutable {
245 if (!result.has_value()) {
246 ASSERT(result.error().isCancellation());
247 handler(WTFMove(result.error()));
248 return;
249 }
250 if (result.value().results.summary.blockedLoad) {
251 handler(this->accessControlErrorForValidationHandler("Blocked by content extension"_s));
252 return;
253 }
254
255 if (!weakThis)
256 return handler({ ResourceError { ResourceError::Type::Cancellation }});
257 this->continueCheckingRequestOrDoSyntheticRedirect(WTFMove(originalRequest), WTFMove(result.value().request), WTFMove(handler));
258 });
259#else
260 this->continueCheckingRequestOrDoSyntheticRedirect(WTFMove(originalRequest), WTFMove(request), WTFMove(handler));
261#endif
262 });
263}
264
265void NetworkLoadChecker::continueCheckingRequestOrDoSyntheticRedirect(ResourceRequest&& originalRequest, ResourceRequest&& currentRequest, ValidationHandler&& handler)
266{
267 // If main frame load and request has been modified, trigger a synthetic redirect.
268 if (m_requestLoadType == LoadType::MainFrame && currentRequest.url() != originalRequest.url()) {
269 ResourceResponse redirectResponse = ResourceResponse::syntheticRedirectResponse(originalRequest.url(), currentRequest.url());
270 handler(RedirectionTriplet { WTFMove(originalRequest), WTFMove(currentRequest), WTFMove(redirectResponse) });
271 return;
272 }
273 this->continueCheckingRequest(WTFMove(currentRequest), WTFMove(handler));
274}
275
276bool NetworkLoadChecker::isAllowedByContentSecurityPolicy(const ResourceRequest& request, WebCore::ContentSecurityPolicyClient* client)
277{
278 auto* contentSecurityPolicy = this->contentSecurityPolicy();
279 contentSecurityPolicy->setClient(client);
280 auto clearContentSecurityPolicyClient = makeScopeExit([&] {
281 contentSecurityPolicy->setClient(nullptr);
282 });
283
284 auto redirectResponseReceived = isRedirected() ? ContentSecurityPolicy::RedirectResponseReceived::Yes : ContentSecurityPolicy::RedirectResponseReceived::No;
285 switch (m_options.destination) {
286 case FetchOptions::Destination::Worker:
287 case FetchOptions::Destination::Serviceworker:
288 case FetchOptions::Destination::Sharedworker:
289 return contentSecurityPolicy->allowChildContextFromSource(request.url(), redirectResponseReceived);
290 case FetchOptions::Destination::Script:
291 if (request.requester() == ResourceRequest::Requester::ImportScripts && !contentSecurityPolicy->allowScriptFromSource(request.url(), redirectResponseReceived))
292 return false;
293 // FIXME: Check CSP for non-importScripts() initiated loads.
294 return true;
295 case FetchOptions::Destination::EmptyString:
296 return contentSecurityPolicy->allowConnectToSource(request.url(), redirectResponseReceived);
297 case FetchOptions::Destination::Audio:
298 case FetchOptions::Destination::Document:
299 case FetchOptions::Destination::Embed:
300 case FetchOptions::Destination::Font:
301 case FetchOptions::Destination::Image:
302 case FetchOptions::Destination::Manifest:
303 case FetchOptions::Destination::Object:
304 case FetchOptions::Destination::Report:
305 case FetchOptions::Destination::Style:
306 case FetchOptions::Destination::Track:
307 case FetchOptions::Destination::Video:
308 case FetchOptions::Destination::Xslt:
309 // FIXME: Check CSP for these destinations.
310 return true;
311 }
312 ASSERT_NOT_REACHED();
313 return true;
314}
315
316void NetworkLoadChecker::continueCheckingRequest(ResourceRequest&& request, ValidationHandler&& handler)
317{
318 if (m_options.credentials == FetchOptions::Credentials::SameOrigin)
319 m_storedCredentialsPolicy = m_isSameOriginRequest && m_origin->canRequest(request.url()) ? StoredCredentialsPolicy::Use : StoredCredentialsPolicy::DoNotUse;
320
321 m_isSameOriginRequest = m_isSameOriginRequest && isSameOrigin(request.url(), m_origin.get());
322
323 if (doesNotNeedCORSCheck(request.url())) {
324 handler(WTFMove(request));
325 return;
326 }
327
328 if (m_options.mode == FetchOptions::Mode::SameOrigin) {
329 String message = makeString("Unsafe attempt to load URL ", request.url().stringCenterEllipsizedToLength(), " from origin ", m_origin->toString(), ". Domains, protocols and ports must match.\n");
330 handler(accessControlErrorForValidationHandler(WTFMove(message)));
331 return;
332 }
333
334 if (isRedirected()) {
335 RELEASE_LOG_IF_ALLOWED("checkRequest - Redirect requires CORS checks");
336 checkCORSRedirectedRequest(WTFMove(request), WTFMove(handler));
337 return;
338 }
339
340 checkCORSRequest(WTFMove(request), WTFMove(handler));
341}
342
343void NetworkLoadChecker::checkCORSRequest(ResourceRequest&& request, ValidationHandler&& handler)
344{
345 ASSERT(m_options.mode == FetchOptions::Mode::Cors);
346
347 // Except in case where preflight is needed, loading should be able to continue on its own.
348 switch (m_preflightPolicy) {
349 case PreflightPolicy::Force:
350 checkCORSRequestWithPreflight(WTFMove(request), WTFMove(handler));
351 break;
352 case PreflightPolicy::Consider:
353 if (!m_isSimpleRequest || !isSimpleCrossOriginAccessRequest(request.httpMethod(), m_originalRequestHeaders)) {
354 checkCORSRequestWithPreflight(WTFMove(request), WTFMove(handler));
355 return;
356 }
357 FALLTHROUGH;
358 case PreflightPolicy::Prevent:
359 updateRequestForAccessControl(request, *m_origin, m_storedCredentialsPolicy);
360 handler(WTFMove(request));
361 break;
362 }
363}
364
365void NetworkLoadChecker::checkCORSRedirectedRequest(ResourceRequest&& request, ValidationHandler&& handler)
366{
367 ASSERT(m_options.mode == FetchOptions::Mode::Cors);
368 ASSERT(isRedirected());
369
370 // Force any subsequent request to use these checks.
371 m_isSameOriginRequest = false;
372
373 if (!m_origin->canRequest(m_previousURL) && !protocolHostAndPortAreEqual(m_previousURL, request.url())) {
374 // Use a unique origin for subsequent loads if needed.
375 // https://fetch.spec.whatwg.org/#concept-http-redirect-fetch (Step 10).
376 if (!m_origin || !m_origin->isUnique())
377 m_origin = SecurityOrigin::createUnique();
378 }
379
380 // FIXME: We should set the request referrer according the referrer policy.
381
382 // Let's fetch the request with the original headers (equivalent to request cloning specified by fetch algorithm).
383 if (!request.httpHeaderFields().contains(HTTPHeaderName::Authorization))
384 m_firstRequestHeaders.remove(HTTPHeaderName::Authorization);
385 request.setHTTPHeaderFields(m_firstRequestHeaders);
386
387 checkCORSRequest(WTFMove(request), WTFMove(handler));
388}
389
390void NetworkLoadChecker::checkCORSRequestWithPreflight(ResourceRequest&& request, ValidationHandler&& handler)
391{
392 ASSERT(m_options.mode == FetchOptions::Mode::Cors);
393
394 m_isSimpleRequest = false;
395 // FIXME: We should probably partition preflight result cache by session ID.
396 if (CrossOriginPreflightResultCache::singleton().canSkipPreflight(m_origin->toString(), request.url(), m_storedCredentialsPolicy, request.httpMethod(), m_originalRequestHeaders)) {
397 RELEASE_LOG_IF_ALLOWED("checkCORSRequestWithPreflight - preflight can be skipped thanks to cached result");
398 updateRequestForAccessControl(request, *m_origin, m_storedCredentialsPolicy);
399 handler(WTFMove(request));
400 return;
401 }
402
403 auto requestForPreflight = request;
404 // We need to set header fields to m_originalRequestHeaders to correctly compute Access-Control-Request-Headers header value.
405 requestForPreflight.setHTTPHeaderFields(m_originalRequestHeaders);
406 NetworkCORSPreflightChecker::Parameters parameters = {
407 WTFMove(requestForPreflight),
408 *m_origin,
409 request.httpReferrer(),
410 request.httpUserAgent(),
411 m_sessionID,
412 m_pageID,
413 m_frameID,
414 m_storedCredentialsPolicy
415 };
416 m_corsPreflightChecker = std::make_unique<NetworkCORSPreflightChecker>(m_networkProcess.get(), WTFMove(parameters), m_shouldCaptureExtraNetworkLoadMetrics, [this, request = WTFMove(request), handler = WTFMove(handler), isRedirected = isRedirected()](auto&& error) mutable {
417 RELEASE_LOG_IF_ALLOWED("checkCORSRequestWithPreflight - makeCrossOriginAccessRequestWithPreflight preflight complete, success: %d forRedirect? %d", error.isNull(), isRedirected);
418
419 if (!error.isNull()) {
420 handler(WTFMove(error));
421 return;
422 }
423
424 if (m_shouldCaptureExtraNetworkLoadMetrics)
425 m_loadInformation.transactions.append(m_corsPreflightChecker->takeInformation());
426
427 auto corsPreflightChecker = WTFMove(m_corsPreflightChecker);
428 updateRequestForAccessControl(request, *m_origin, m_storedCredentialsPolicy);
429 handler(WTFMove(request));
430 });
431 m_corsPreflightChecker->startPreflight();
432}
433
434bool NetworkLoadChecker::doesNotNeedCORSCheck(const URL& url) const
435{
436 if (m_options.mode == FetchOptions::Mode::NoCors || m_options.mode == FetchOptions::Mode::Navigate)
437 return true;
438
439 if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(url.protocol().toStringWithoutCopying()))
440 return true;
441
442 return m_isSameOriginRequest;
443}
444
445ContentSecurityPolicy* NetworkLoadChecker::contentSecurityPolicy()
446{
447 if (!m_contentSecurityPolicy && m_cspResponseHeaders) {
448 // FIXME: Pass the URL of the protected resource instead of its origin.
449 m_contentSecurityPolicy = std::make_unique<ContentSecurityPolicy>(URL { URL { }, m_origin->toString() });
450 m_contentSecurityPolicy->didReceiveHeaders(*m_cspResponseHeaders, String { m_referrer }, ContentSecurityPolicy::ReportParsingErrors::No);
451 }
452 return m_contentSecurityPolicy.get();
453}
454
455#if ENABLE(CONTENT_EXTENSIONS)
456void NetworkLoadChecker::processContentRuleListsForLoad(ResourceRequest&& request, ContentExtensionCallback&& callback)
457{
458 // FIXME: Enable content blockers for navigation loads.
459 if (!m_checkContentExtensions || !m_userContentControllerIdentifier || m_options.mode == FetchOptions::Mode::Navigate) {
460 ContentRuleListResults results;
461 callback(ContentExtensionResult { WTFMove(request), results });
462 return;
463 }
464
465 m_networkProcess->networkContentRuleListManager().contentExtensionsBackend(*m_userContentControllerIdentifier, [this, weakThis = makeWeakPtr(this), request = WTFMove(request), callback = WTFMove(callback)](auto& backend) mutable {
466 if (!weakThis) {
467 callback(makeUnexpected(ResourceError { ResourceError::Type::Cancellation }));
468 return;
469 }
470
471 auto results = backend.processContentRuleListsForPingLoad(request.url(), m_mainDocumentURL);
472 WebCore::ContentExtensions::applyResultsToRequest(ContentRuleListResults { results }, nullptr, request);
473 callback(ContentExtensionResult { WTFMove(request), results });
474 });
475}
476#endif // ENABLE(CONTENT_EXTENSIONS)
477
478void NetworkLoadChecker::storeRedirectionIfNeeded(const ResourceRequest& request, const ResourceResponse& response)
479{
480 if (!m_shouldCaptureExtraNetworkLoadMetrics)
481 return;
482 m_loadInformation.transactions.append(NetworkTransactionInformation { NetworkTransactionInformation::Type::Redirection, ResourceRequest { request }, ResourceResponse { response }, { } });
483}
484
485} // namespace WebKit
486
487#undef RELEASE_LOG_IF_ALLOWED
488