source: webkit/trunk/Source/WebCore/loader/CrossOriginAccessControl.cpp

Last change on this file was 295473, checked in by [email protected], 3 years ago

CORS checks shouldn't unblock cookies
https://bugs.webkit.org/show_bug.cgi?id=241527

Reviewed by Brent Fulgham.

If cookies have been blocked, a request that has been made with {credentials: 'include'} shouldn't un-block the cookies.

  • Source/WebCore/loader/CrossOriginAccessControl.cpp:

(WebCore::updateRequestForAccessControl):

  • Tools/TestWebKitAPI/Tests/WebKitCocoa/WKContentExtensionStore.mm:

(TEST_F):

Canonical link: https://commits.webkit.org/251478@main

  • Property svn:eol-style set to native
File size: 15.7 KB
Line 
1/*
2 * Copyright (C) 2008-2021 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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 *
25 */
26
27#include "config.h"
28#include "CrossOriginAccessControl.h"
29
30#include "CachedResourceRequest.h"
31#include "CrossOriginEmbedderPolicy.h"
32#include "CrossOriginPreflightResultCache.h"
33#include "DocumentLoader.h"
34#include "HTTPHeaderNames.h"
35#include "HTTPParsers.h"
36#include "LegacySchemeRegistry.h"
37#include "Page.h"
38#include "ResourceRequest.h"
39#include "ResourceResponse.h"
40#include "RuntimeApplicationChecks.h"
41#include "SecurityOrigin.h"
42#include "SecurityPolicy.h"
43#include <mutex>
44#include <wtf/NeverDestroyed.h>
45#include <wtf/text/AtomString.h>
46#include <wtf/text/StringBuilder.h>
47
48namespace WebCore {
49
50bool isOnAccessControlSimpleRequestMethodAllowlist(const String& method)
51{
52 return method == "GET"_s || method == "HEAD"_s || method == "POST"_s;
53}
54
55bool isSimpleCrossOriginAccessRequest(const String& method, const HTTPHeaderMap& headerMap)
56{
57 if (!isOnAccessControlSimpleRequestMethodAllowlist(method))
58 return false;
59
60 for (const auto& header : headerMap) {
61 if (!header.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(header.keyAsHTTPHeaderName.value(), header.value))
62 return false;
63 }
64
65 return true;
66}
67
68void updateRequestReferrer(ResourceRequest& request, ReferrerPolicy referrerPolicy, const String& outgoingReferrer)
69{
70 String newOutgoingReferrer = SecurityPolicy::generateReferrerHeader(referrerPolicy, request.url(), outgoingReferrer);
71 if (newOutgoingReferrer.isEmpty())
72 request.clearHTTPReferrer();
73 else
74 request.setHTTPReferrer(newOutgoingReferrer);
75}
76
77void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin& securityOrigin, StoredCredentialsPolicy storedCredentialsPolicy)
78{
79 request.removeCredentials();
80 if (request.allowCookies())
81 request.setAllowCookies(storedCredentialsPolicy == StoredCredentialsPolicy::Use);
82 request.setHTTPOrigin(securityOrigin.toString());
83}
84
85ResourceRequest createAccessControlPreflightRequest(const ResourceRequest& request, SecurityOrigin& securityOrigin, const String& referrer)
86{
87 ResourceRequest preflightRequest(request.url());
88 static const double platformDefaultTimeout = 0;
89 preflightRequest.setTimeoutInterval(platformDefaultTimeout);
90 updateRequestForAccessControl(preflightRequest, securityOrigin, StoredCredentialsPolicy::DoNotUse);
91 preflightRequest.setHTTPMethod("OPTIONS"_s);
92 preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestMethod, request.httpMethod());
93 preflightRequest.setPriority(request.priority());
94 preflightRequest.setFirstPartyForCookies(request.firstPartyForCookies());
95 preflightRequest.setIsAppInitiated(request.isAppInitiated());
96 if (!referrer.isNull())
97 preflightRequest.setHTTPReferrer(referrer);
98
99 const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
100
101 if (!requestHeaderFields.isEmpty()) {
102 Vector<String> unsafeHeaders;
103 for (auto& headerField : requestHeaderFields) {
104 if (!headerField.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(*headerField.keyAsHTTPHeaderName, headerField.value))
105 unsafeHeaders.append(headerField.key.convertToASCIILowercase());
106 }
107
108 std::sort(unsafeHeaders.begin(), unsafeHeaders.end(), WTF::codePointCompareLessThan);
109
110 StringBuilder headerBuffer;
111
112 bool appendComma = false;
113 for (const auto& headerField : unsafeHeaders) {
114 if (appendComma)
115 headerBuffer.append(',');
116 else
117 appendComma = true;
118
119 headerBuffer.append(headerField);
120 }
121 if (!headerBuffer.isEmpty())
122 preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestHeaders, headerBuffer.toString());
123 }
124
125 return preflightRequest;
126}
127
128// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#create-a-potential-cors-request
129CachedResourceRequest createPotentialAccessControlRequest(ResourceRequest&& request, ResourceLoaderOptions&& options, Document& document, const String& crossOriginAttribute, SameOriginFlag sameOriginFlag)
130{
131 ASSERT(options.mode == FetchOptions::Mode::NoCors);
132 if (!crossOriginAttribute.isNull())
133 options.mode = FetchOptions::Mode::Cors;
134 else if (sameOriginFlag == SameOriginFlag::Yes)
135 options.mode = FetchOptions::Mode::SameOrigin;
136
137 if (options.mode != FetchOptions::Mode::NoCors) {
138 if (auto* page = document.page()) {
139 if (page->shouldDisableCorsForRequestTo(request.url()))
140 options.mode = FetchOptions::Mode::NoCors;
141 }
142 }
143
144 if (auto* documentLoader = document.loader())
145 request.setIsAppInitiated(documentLoader->lastNavigationWasAppInitiated());
146
147 if (crossOriginAttribute.isNull()) {
148 CachedResourceRequest cachedRequest { WTFMove(request), WTFMove(options) };
149 cachedRequest.setOrigin(document.securityOrigin());
150 return cachedRequest;
151 }
152
153 FetchOptions::Credentials credentials = equalLettersIgnoringASCIICase(crossOriginAttribute, "omit"_s)
154 ? FetchOptions::Credentials::Omit : equalLettersIgnoringASCIICase(crossOriginAttribute, "use-credentials"_s)
155 ? FetchOptions::Credentials::Include : FetchOptions::Credentials::SameOrigin;
156 options.credentials = credentials;
157 switch (credentials) {
158 case FetchOptions::Credentials::Include:
159 options.storedCredentialsPolicy = StoredCredentialsPolicy::Use;
160 break;
161 case FetchOptions::Credentials::SameOrigin:
162 options.storedCredentialsPolicy = document.securityOrigin().canRequest(request.url()) ? StoredCredentialsPolicy::Use : StoredCredentialsPolicy::DoNotUse;
163 break;
164 case FetchOptions::Credentials::Omit:
165 options.storedCredentialsPolicy = StoredCredentialsPolicy::DoNotUse;
166 }
167
168 CachedResourceRequest cachedRequest { WTFMove(request), WTFMove(options) };
169 updateRequestForAccessControl(cachedRequest.resourceRequest(), document.securityOrigin(), options.storedCredentialsPolicy);
170 return cachedRequest;
171}
172
173String validateCrossOriginRedirectionURL(const URL& redirectURL)
174{
175 if (!LegacySchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(redirectURL.protocol()))
176 return makeString("not allowed to follow a cross-origin CORS redirection with non CORS scheme");
177
178 if (redirectURL.hasCredentials())
179 return makeString("redirection URL ", redirectURL.string(), " has credentials");
180
181 return { };
182}
183
184OptionSet<HTTPHeadersToKeepFromCleaning> httpHeadersToKeepFromCleaning(const HTTPHeaderMap& headers)
185{
186 OptionSet<HTTPHeadersToKeepFromCleaning> headersToKeep;
187 if (headers.contains(HTTPHeaderName::ContentType))
188 headersToKeep.add(HTTPHeadersToKeepFromCleaning::ContentType);
189 if (headers.contains(HTTPHeaderName::Referer))
190 headersToKeep.add(HTTPHeadersToKeepFromCleaning::Referer);
191 if (headers.contains(HTTPHeaderName::Origin))
192 headersToKeep.add(HTTPHeadersToKeepFromCleaning::Origin);
193 if (headers.contains(HTTPHeaderName::UserAgent))
194 headersToKeep.add(HTTPHeadersToKeepFromCleaning::UserAgent);
195 if (headers.contains(HTTPHeaderName::AcceptEncoding))
196 headersToKeep.add(HTTPHeadersToKeepFromCleaning::AcceptEncoding);
197 if (headers.contains(HTTPHeaderName::CacheControl))
198 headersToKeep.add(HTTPHeadersToKeepFromCleaning::CacheControl);
199 return headersToKeep;
200}
201
202void cleanHTTPRequestHeadersForAccessControl(ResourceRequest& request, OptionSet<HTTPHeadersToKeepFromCleaning> headersToKeep)
203{
204 // Remove headers that may have been added by the network layer that cause access control to fail.
205 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::ContentType)) {
206 auto contentType = request.httpContentType();
207 if (!contentType.isNull() && !isCrossOriginSafeRequestHeader(HTTPHeaderName::ContentType, contentType))
208 request.clearHTTPContentType();
209 }
210 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::Referer))
211 request.clearHTTPReferrer();
212 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::Origin))
213 request.clearHTTPOrigin();
214 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::UserAgent))
215 request.clearHTTPUserAgent();
216 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::AcceptEncoding))
217 request.clearHTTPAcceptEncoding();
218 if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::CacheControl))
219 request.removeHTTPHeaderField(HTTPHeaderName::CacheControl);
220 request.removeHTTPHeaderField(HTTPHeaderName::SecFetchDest);
221 request.removeHTTPHeaderField(HTTPHeaderName::SecFetchMode);
222}
223
224CrossOriginAccessControlCheckDisabler& CrossOriginAccessControlCheckDisabler::singleton()
225{
226 ASSERT(!isInNetworkProcess());
227 static NeverDestroyed<CrossOriginAccessControlCheckDisabler> disabler;
228 return disabler.get();
229}
230
231void CrossOriginAccessControlCheckDisabler::setCrossOriginAccessControlCheckEnabled(bool enabled)
232{
233 ASSERT(!isInNetworkProcess());
234 m_accessControlCheckEnabled = enabled;
235}
236
237bool CrossOriginAccessControlCheckDisabler::crossOriginAccessControlCheckEnabled() const
238{
239 ASSERT(!isInNetworkProcess());
240 return m_accessControlCheckEnabled;
241}
242
243Expected<void, String> passesAccessControlCheck(const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, const SecurityOrigin& securityOrigin, const CrossOriginAccessControlCheckDisabler* checkDisabler)
244{
245 // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent,
246 // even with Access-Control-Allow-Credentials set to true.
247 const String& accessControlOriginString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowOrigin);
248 bool starAllowed = storedCredentialsPolicy == StoredCredentialsPolicy::DoNotUse;
249 if (!starAllowed)
250 starAllowed = checkDisabler && !checkDisabler->crossOriginAccessControlCheckEnabled();
251 if (accessControlOriginString == "*"_s && starAllowed)
252 return { };
253
254 String securityOriginString = securityOrigin.toString();
255 if (accessControlOriginString != securityOriginString) {
256 if (accessControlOriginString == "*"_s)
257 return makeUnexpected("Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true."_s);
258 if (accessControlOriginString.find(',') != notFound)
259 return makeUnexpected("Access-Control-Allow-Origin cannot contain more than one origin."_s);
260 return makeUnexpected(makeString("Origin ", securityOriginString, " is not allowed by Access-Control-Allow-Origin.", " Status code: ", response.httpStatusCode()));
261 }
262
263 if (storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
264 const String& accessControlCredentialsString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowCredentials);
265 if (accessControlCredentialsString != "true"_s)
266 return makeUnexpected("Credentials flag is true, but Access-Control-Allow-Credentials is not \"true\"."_s);
267 }
268
269 return { };
270}
271
272Expected<void, String> validatePreflightResponse(PAL::SessionID sessionID, const ResourceRequest& request, const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, const SecurityOrigin& securityOrigin, const CrossOriginAccessControlCheckDisabler* checkDisabler)
273{
274 if (!response.isSuccessful())
275 return makeUnexpected(makeString("Preflight response is not successful. Status code: ", response.httpStatusCode()));
276
277 auto accessControlCheckResult = passesAccessControlCheck(response, storedCredentialsPolicy, securityOrigin, checkDisabler);
278 if (!accessControlCheckResult)
279 return accessControlCheckResult;
280
281 auto result = CrossOriginPreflightResultCacheItem::create(storedCredentialsPolicy, response);
282 if (!result.has_value())
283 return makeUnexpected(WTFMove(result.error()));
284
285 auto entry = WTFMove(result.value());
286 auto errorDescription = entry->validateMethodAndHeaders(request.httpMethod(), request.httpHeaderFields());
287 CrossOriginPreflightResultCache::singleton().appendEntry(sessionID, securityOrigin.toString(), request.url(), entry.moveToUniquePtr());
288
289 if (errorDescription)
290 return makeUnexpected(WTFMove(*errorDescription));
291
292 return { };
293}
294
295// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check
296static inline bool shouldCrossOriginResourcePolicyCancelLoad(CrossOriginEmbedderPolicyValue coep, const SecurityOrigin& origin, const ResourceResponse& response, ForNavigation forNavigation)
297{
298 if (forNavigation == ForNavigation::Yes && coep != CrossOriginEmbedderPolicyValue::RequireCORP)
299 return false;
300
301 if (response.isNull() || origin.canRequest(response.url()))
302 return false;
303
304 auto policy = parseCrossOriginResourcePolicyHeader(response.httpHeaderField(HTTPHeaderName::CrossOriginResourcePolicy));
305
306 // https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check (step 4).
307 if ((policy == CrossOriginResourcePolicy::None || policy == CrossOriginResourcePolicy::Invalid) && coep == CrossOriginEmbedderPolicyValue::RequireCORP)
308 return true;
309
310 if (policy == CrossOriginResourcePolicy::SameOrigin)
311 return true;
312
313 if (policy == CrossOriginResourcePolicy::SameSite) {
314 if (origin.isUnique())
315 return true;
316#if ENABLE(PUBLIC_SUFFIX_LIST)
317 if (!RegistrableDomain::uncheckedCreateFromHost(origin.host()).matches(response.url()))
318 return true;
319#endif
320 if (origin.protocol() == "http"_s && response.url().protocol() == "https"_s)
321 return true;
322 }
323
324 return false;
325}
326
327std::optional<ResourceError> validateCrossOriginResourcePolicy(CrossOriginEmbedderPolicyValue coep, const SecurityOrigin& origin, const URL& requestURL, const ResourceResponse& response, ForNavigation forNavigation)
328{
329 if (shouldCrossOriginResourcePolicyCancelLoad(coep, origin, response, forNavigation))
330 return ResourceError { errorDomainWebKitInternal, 0, requestURL, makeString("Cancelled load to ", response.url().stringCenterEllipsizedToLength(), " because it violates the resource's Cross-Origin-Resource-Policy response header."), ResourceError::Type::AccessControl };
331 return std::nullopt;
332}
333
334std::optional<ResourceError> validateRangeRequestedFlag(const ResourceRequest& request, const ResourceResponse& response)
335{
336 if (response.isRangeRequested() && response.httpStatusCode() == 206 && response.type() == ResourceResponse::Type::Opaque && !request.hasHTTPHeaderField(HTTPHeaderName::Range))
337 return ResourceError({ }, 0, response.url(), { }, ResourceError::Type::General);
338 return std::nullopt;
339}
340
341} // namespace WebCore
Note: See TracBrowser for help on using the repository browser.