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
44namespace WebCore {
45
46bool isOnAccessControlSimpleRequestMethodWhitelist(const String& method)
47{
48 return method == "GET" || method == "HEAD" || method == "POST";
49}
50
51bool 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
64void 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
73void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin& securityOrigin, StoredCredentialsPolicy storedCredentialsPolicy)
74{
75 request.removeCredentials();
76 request.setAllowCookies(storedCredentialsPolicy == StoredCredentialsPolicy::Use);
77 request.setHTTPOrigin(securityOrigin.toString());
78}
79
80ResourceRequest 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
121CachedResourceRequest 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
130bool isValidCrossOriginRedirectionURL(const URL& redirectURL)
131{
132 return SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(redirectURL.protocol().toStringWithoutCopying())
133 && redirectURL.user().isEmpty()
134 && redirectURL.pass().isEmpty();
135}
136
137HTTPHeaderNameSet 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
153void 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
168bool 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
198bool 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
219static 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 (!RegistrableDomain::uncheckedCreateFromHost(origin.host()).matches(response.url()))
234 return true;
235#endif
236 if (origin.protocol() == "http" && response.url().protocol() == "https")
237 return true;
238 }
239
240 return false;
241}
242
243Optional<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
251