48c741a71d6d5531c37b0ac09d21fd6725347b26
[WebKit-https.git] / Source / WebCore / loader / CrossOriginAccessControl.cpp
1 /*
2  * Copyright (C) 2008 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 "CrossOriginPreflightResultCache.h"
32 #include "HTTPHeaderNames.h"
33 #include "HTTPParsers.h"
34 #include "ResourceRequest.h"
35 #include "ResourceResponse.h"
36 #include "SchemeRegistry.h"
37 #include "SecurityOrigin.h"
38 #include "SecurityPolicy.h"
39 #include <mutex>
40 #include <wtf/NeverDestroyed.h>
41 #include <wtf/text/AtomicString.h>
42 #include <wtf/text/StringBuilder.h>
43
44 namespace WebCore {
45
46 bool isOnAccessControlSimpleRequestMethodWhitelist(const String& method)
47 {
48     return method == "GET" || method == "HEAD" || method == "POST";
49 }
50
51 bool isSimpleCrossOriginAccessRequest(const String& method, const HTTPHeaderMap& headerMap)
52 {
53     if (!isOnAccessControlSimpleRequestMethodWhitelist(method))
54         return false;
55
56     for (const auto& header : headerMap) {
57         if (!header.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(header.keyAsHTTPHeaderName.value(), header.value))
58             return false;
59     }
60
61     return true;
62 }
63
64 void updateRequestReferrer(ResourceRequest& request, ReferrerPolicy referrerPolicy, const String& outgoingReferrer)
65 {
66     String newOutgoingReferrer = SecurityPolicy::generateReferrerHeader(referrerPolicy, request.url(), outgoingReferrer);
67     if (newOutgoingReferrer.isEmpty())
68         request.clearHTTPReferrer();
69     else
70         request.setHTTPReferrer(newOutgoingReferrer);
71 }
72
73 void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin& securityOrigin, StoredCredentialsPolicy storedCredentialsPolicy)
74 {
75     request.removeCredentials();
76     request.setAllowCookies(storedCredentialsPolicy == StoredCredentialsPolicy::Use);
77     request.setHTTPOrigin(securityOrigin.toString());
78 }
79
80 ResourceRequest createAccessControlPreflightRequest(const ResourceRequest& request, SecurityOrigin& securityOrigin, const String& referrer)
81 {
82     ResourceRequest preflightRequest(request.url());
83     static const double platformDefaultTimeout = 0;
84     preflightRequest.setTimeoutInterval(platformDefaultTimeout);
85     updateRequestForAccessControl(preflightRequest, securityOrigin, StoredCredentialsPolicy::DoNotUse);
86     preflightRequest.setHTTPMethod("OPTIONS");
87     preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestMethod, request.httpMethod());
88     preflightRequest.setPriority(request.priority());
89     if (!referrer.isNull())
90         preflightRequest.setHTTPReferrer(referrer);
91
92     const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
93
94     if (!requestHeaderFields.isEmpty()) {
95         Vector<String> unsafeHeaders;
96         for (auto& headerField : requestHeaderFields) {
97             if (!headerField.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(*headerField.keyAsHTTPHeaderName, headerField.value))
98                 unsafeHeaders.append(headerField.key.convertToASCIILowercase());
99         }
100
101         std::sort(unsafeHeaders.begin(), unsafeHeaders.end(), WTF::codePointCompareLessThan);
102
103         StringBuilder headerBuffer;
104
105         bool appendComma = false;
106         for (const auto& headerField : unsafeHeaders) {
107             if (appendComma)
108                 headerBuffer.append(',');
109             else
110                 appendComma = true;
111
112             headerBuffer.append(headerField);
113         }
114         if (!headerBuffer.isEmpty())
115             preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestHeaders, headerBuffer.toString());
116     }
117
118     return preflightRequest;
119 }
120
121 CachedResourceRequest createPotentialAccessControlRequest(ResourceRequest&& request, Document& document, const String& crossOriginAttribute, ResourceLoaderOptions&& options)
122 {
123     // FIXME: This does not match the algorithm "create a potential-CORS request":
124     // <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#create-a-potential-cors-request> (31 August 2018).
125     auto cachedRequest = CachedResourceRequest { WTFMove(request), WTFMove(options) };
126     cachedRequest.deprecatedSetAsPotentiallyCrossOrigin(crossOriginAttribute, document);
127     return cachedRequest;
128 }
129
130 bool isValidCrossOriginRedirectionURL(const URL& redirectURL)
131 {
132     return SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(redirectURL.protocol().toStringWithoutCopying())
133         && redirectURL.user().isEmpty()
134         && redirectURL.pass().isEmpty();
135 }
136
137 HTTPHeaderNameSet httpHeadersToKeepFromCleaning(const HTTPHeaderMap& headers)
138 {
139     HTTPHeaderNameSet headersToKeep;
140     if (headers.contains(HTTPHeaderName::ContentType))
141         headersToKeep.add(HTTPHeaderName::ContentType);
142     if (headers.contains(HTTPHeaderName::Referer))
143         headersToKeep.add(HTTPHeaderName::Referer);
144     if (headers.contains(HTTPHeaderName::Origin))
145         headersToKeep.add(HTTPHeaderName::Origin);
146     if (headers.contains(HTTPHeaderName::UserAgent))
147         headersToKeep.add(HTTPHeaderName::UserAgent);
148     if (headers.contains(HTTPHeaderName::AcceptEncoding))
149         headersToKeep.add(HTTPHeaderName::AcceptEncoding);
150     return headersToKeep;
151 }
152
153 void cleanHTTPRequestHeadersForAccessControl(ResourceRequest& request, const HashSet<HTTPHeaderName, WTF::IntHash<HTTPHeaderName>, WTF::StrongEnumHashTraits<HTTPHeaderName>>& headersToKeep)
154 {
155     // Remove headers that may have been added by the network layer that cause access control to fail.
156     if (!headersToKeep.contains(HTTPHeaderName::ContentType) && !isCrossOriginSafeRequestHeader(HTTPHeaderName::ContentType, request.httpContentType()))
157         request.clearHTTPContentType();
158     if (!headersToKeep.contains(HTTPHeaderName::Referer))
159         request.clearHTTPReferrer();
160     if (!headersToKeep.contains(HTTPHeaderName::Origin))
161         request.clearHTTPOrigin();
162     if (!headersToKeep.contains(HTTPHeaderName::UserAgent))
163         request.clearHTTPUserAgent();
164     if (!headersToKeep.contains(HTTPHeaderName::AcceptEncoding))
165         request.clearHTTPAcceptEncoding();
166 }
167
168 bool passesAccessControlCheck(const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, SecurityOrigin& securityOrigin, String& errorDescription)
169 {
170     // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent,
171     // even with Access-Control-Allow-Credentials set to true.
172     const String& accessControlOriginString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowOrigin);
173     if (accessControlOriginString == "*" && storedCredentialsPolicy == StoredCredentialsPolicy::DoNotUse)
174         return true;
175
176     String securityOriginString = securityOrigin.toString();
177     if (accessControlOriginString != securityOriginString) {
178         if (accessControlOriginString == "*")
179             errorDescription = "Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true."_s;
180         else if (accessControlOriginString.find(',') != notFound)
181             errorDescription = "Access-Control-Allow-Origin cannot contain more than one origin."_s;
182         else
183             errorDescription = makeString("Origin ", securityOriginString, " is not allowed by Access-Control-Allow-Origin.");
184         return false;
185     }
186
187     if (storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
188         const String& accessControlCredentialsString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowCredentials);
189         if (accessControlCredentialsString != "true") {
190             errorDescription = "Credentials flag is true, but Access-Control-Allow-Credentials is not \"true\".";
191             return false;
192         }
193     }
194
195     return true;
196 }
197
198 bool validatePreflightResponse(const ResourceRequest& request, const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, SecurityOrigin& securityOrigin, String& errorDescription)
199 {
200     if (!response.isSuccessful()) {
201         errorDescription = "Preflight response is not successful"_s;
202         return false;
203     }
204
205     if (!passesAccessControlCheck(response, storedCredentialsPolicy, securityOrigin, errorDescription))
206         return false;
207
208     auto result = std::make_unique<CrossOriginPreflightResultCacheItem>(storedCredentialsPolicy);
209     if (!result->parse(response)
210         || !result->allowsCrossOriginMethod(request.httpMethod(), errorDescription)
211         || !result->allowsCrossOriginHeaders(request.httpHeaderFields(), errorDescription)) {
212         return false;
213     }
214
215     CrossOriginPreflightResultCache::singleton().appendEntry(securityOrigin.toString(), request.url(), WTFMove(result));
216     return true;
217 }
218
219 static inline bool shouldCrossOriginResourcePolicyCancelLoad(const SecurityOrigin& origin, const ResourceResponse& response)
220 {
221     if (origin.canRequest(response.url()))
222         return false;
223
224     auto policy = parseCrossOriginResourcePolicyHeader(response.httpHeaderField(HTTPHeaderName::CrossOriginResourcePolicy));
225
226     if (policy == CrossOriginResourcePolicy::SameOrigin)
227         return true;
228
229     if (policy == CrossOriginResourcePolicy::SameSite) {
230         if (origin.isUnique())
231             return true;
232 #if ENABLE(PUBLIC_SUFFIX_LIST)
233         if (!registrableDomainsAreEqual(response.url(), ResourceRequest::partitionName(origin.host())))
234             return true;
235 #endif
236         if (origin.protocol() == "http" && response.url().protocol() == "https")
237             return true;
238     }
239
240     return false;
241 }
242
243 Optional<ResourceError> validateCrossOriginResourcePolicy(const SecurityOrigin& origin, const URL& requestURL, const ResourceResponse& response)
244 {
245     if (shouldCrossOriginResourcePolicyCancelLoad(origin, response))
246         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 };
247     return WTF::nullopt;
248 }
249
250 } // namespace WebCore