2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, 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 COMPUTER, 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.
28 #include "core/fetch/CrossOriginAccessControl.h"
30 #include "core/fetch/Resource.h"
31 #include "core/fetch/ResourceLoaderOptions.h"
32 #include "platform/network/HTTPParsers.h"
33 #include "platform/network/ResourceRequest.h"
34 #include "platform/network/ResourceResponse.h"
35 #include "platform/weborigin/SchemeRegistry.h"
36 #include "platform/weborigin/SecurityOrigin.h"
37 #include "wtf/Threading.h"
38 #include "wtf/text/AtomicString.h"
39 #include "wtf/text/StringBuilder.h"
44 static PassOwnPtr
<HTTPHeaderSet
> createAllowedCrossOriginResponseHeadersSet()
46 OwnPtr
<HTTPHeaderSet
> headerSet
= adoptPtr(new HashSet
<String
, CaseFoldingHash
>);
48 headerSet
->add("cache-control");
49 headerSet
->add("content-language");
50 headerSet
->add("content-type");
51 headerSet
->add("expires");
52 headerSet
->add("last-modified");
53 headerSet
->add("pragma");
55 return headerSet
.release();
58 bool isOnAccessControlResponseHeaderWhitelist(const String
& name
)
60 AtomicallyInitializedStaticReference(HTTPHeaderSet
, allowedCrossOriginResponseHeaders
, (createAllowedCrossOriginResponseHeadersSet().leakPtr()));
62 return allowedCrossOriginResponseHeaders
.contains(name
);
65 void updateRequestForAccessControl(ResourceRequest
& request
, SecurityOrigin
* securityOrigin
, StoredCredentials allowCredentials
)
67 request
.removeCredentials();
68 request
.setAllowStoredCredentials(allowCredentials
== AllowStoredCredentials
);
69 request
.setFetchCredentialsMode(allowCredentials
== AllowStoredCredentials
? WebURLRequest::FetchCredentialsModeInclude
: WebURLRequest::FetchCredentialsModeOmit
);
72 request
.setHTTPOrigin(securityOrigin
->toAtomicString());
75 ResourceRequest
createAccessControlPreflightRequest(const ResourceRequest
& request
, SecurityOrigin
* securityOrigin
)
77 ResourceRequest
preflightRequest(request
.url());
78 updateRequestForAccessControl(preflightRequest
, securityOrigin
, DoNotAllowStoredCredentials
);
79 preflightRequest
.setHTTPMethod("OPTIONS");
80 preflightRequest
.setHTTPHeaderField("Access-Control-Request-Method", request
.httpMethod());
81 preflightRequest
.setPriority(request
.priority());
82 preflightRequest
.setRequestContext(request
.requestContext());
83 preflightRequest
.setSkipServiceWorker(true);
85 const HTTPHeaderMap
& requestHeaderFields
= request
.httpHeaderFields();
87 if (requestHeaderFields
.size() > 0) {
88 // Sort header names lexicographically: https://crbug.com/452391
90 // https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
91 Vector
<String
> headers
;
92 for (const auto& header
: requestHeaderFields
) {
93 if (equalIgnoringCase(header
.key
, "referer")) {
94 // When the request is from a Worker, referrer header was added
95 // by WorkerThreadableLoader. But it should not be added to
96 // Access-Control-Request-Headers header.
99 headers
.append(header
.key
.lower());
101 std::sort(headers
.begin(), headers
.end(), WTF::codePointCompareLessThan
);
102 StringBuilder headerBuffer
;
103 for (const String
& header
: headers
) {
104 if (!headerBuffer
.isEmpty())
105 headerBuffer
.appendLiteral(", ");
106 headerBuffer
.append(header
);
108 preflightRequest
.setHTTPHeaderField("Access-Control-Request-Headers", AtomicString(headerBuffer
.toString()));
111 return preflightRequest
;
114 static bool isOriginSeparator(UChar ch
)
116 return isASCIISpace(ch
) || ch
== ',';
119 static bool isInterestingStatusCode(int statusCode
)
121 // Predicate that gates what status codes should be included in
122 // console error messages for responses containing no access
124 return statusCode
>= 400;
127 static String
buildAccessControlFailureMessage(const String
& detail
, SecurityOrigin
* securityOrigin
)
129 return detail
+ " Origin '" + securityOrigin
->toString() + "' is therefore not allowed access.";
132 bool passesAccessControlCheck(const ResourceResponse
& response
, StoredCredentials includeCredentials
, SecurityOrigin
* securityOrigin
, String
& errorDescription
, WebURLRequest::RequestContext context
)
134 AtomicallyInitializedStaticReference(AtomicString
, allowOriginHeaderName
, (new AtomicString("access-control-allow-origin", AtomicString::ConstructFromLiteral
)));
135 AtomicallyInitializedStaticReference(AtomicString
, allowCredentialsHeaderName
, (new AtomicString("access-control-allow-credentials", AtomicString::ConstructFromLiteral
)));
137 int statusCode
= response
.httpStatusCode();
140 errorDescription
= buildAccessControlFailureMessage("Invalid response.", securityOrigin
);
144 const AtomicString
& allowOriginHeaderValue
= response
.httpHeaderField(allowOriginHeaderName
);
145 if (allowOriginHeaderValue
== starAtom
) {
146 // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent,
147 // even with Access-Control-Allow-Credentials set to true.
148 if (includeCredentials
== DoNotAllowStoredCredentials
)
150 if (response
.isHTTP()) {
151 errorDescription
= buildAccessControlFailureMessage("A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true.", securityOrigin
);
154 } else if (allowOriginHeaderValue
!= securityOrigin
->toAtomicString()) {
155 if (allowOriginHeaderValue
.isNull()) {
156 errorDescription
= buildAccessControlFailureMessage("No 'Access-Control-Allow-Origin' header is present on the requested resource.", securityOrigin
);
158 if (isInterestingStatusCode(statusCode
))
159 errorDescription
.append(" The response had HTTP status code " + String::number(statusCode
) + ".");
161 if (context
== WebURLRequest::RequestContextFetch
)
162 errorDescription
.append(" If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.");
168 if (allowOriginHeaderValue
.string().find(isOriginSeparator
, 0) != kNotFound
) {
169 detail
= "The 'Access-Control-Allow-Origin' header contains multiple values '" + allowOriginHeaderValue
+ "', but only one is allowed.";
171 KURL
headerOrigin(KURL(), allowOriginHeaderValue
);
172 if (!headerOrigin
.isValid())
173 detail
= "The 'Access-Control-Allow-Origin' header contains the invalid value '" + allowOriginHeaderValue
+ "'.";
175 detail
= "The 'Access-Control-Allow-Origin' header has a value '" + allowOriginHeaderValue
+ "' that is not equal to the supplied origin.";
177 errorDescription
= buildAccessControlFailureMessage(detail
, securityOrigin
);
178 if (context
== WebURLRequest::RequestContextFetch
)
179 errorDescription
.append(" Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.");
183 if (includeCredentials
== AllowStoredCredentials
) {
184 const AtomicString
& allowCredentialsHeaderValue
= response
.httpHeaderField(allowCredentialsHeaderName
);
185 if (allowCredentialsHeaderValue
!= "true") {
186 errorDescription
= buildAccessControlFailureMessage("Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is '" + allowCredentialsHeaderValue
+ "'. It must be 'true' to allow credentials.", securityOrigin
);
194 bool passesPreflightStatusCheck(const ResourceResponse
& response
, String
& errorDescription
)
196 // CORS preflight with 3XX is considered network error in
198 // https://fetch.spec.whatwg.org/#cors-preflight-fetch
200 // http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
201 // https://crbug.com/452394
202 if (response
.httpStatusCode() < 200 || response
.httpStatusCode() >= 300) {
203 errorDescription
= "Response for preflight has invalid HTTP status code " + String::number(response
.httpStatusCode());
210 void parseAccessControlExposeHeadersAllowList(const String
& headerValue
, HTTPHeaderSet
& headerSet
)
212 Vector
<String
> headers
;
213 headerValue
.split(',', false, headers
);
214 for (unsigned headerCount
= 0; headerCount
< headers
.size(); headerCount
++) {
215 String strippedHeader
= headers
[headerCount
].stripWhiteSpace();
216 if (!strippedHeader
.isEmpty())
217 headerSet
.add(strippedHeader
);
221 bool CrossOriginAccessControl::isLegalRedirectLocation(const KURL
& requestURL
, String
& errorDescription
)
223 // CORS restrictions imposed on Location: URL -- http://www.w3.org/TR/cors/#redirect-steps (steps 2 + 3.)
224 if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL
.protocol())) {
225 errorDescription
= "The request was redirected to a URL ('" + requestURL
.string() + "') which has a disallowed scheme for cross-origin requests.";
229 if (!(requestURL
.user().isEmpty() && requestURL
.pass().isEmpty())) {
230 errorDescription
= "The request was redirected to a URL ('" + requestURL
.string() + "') containing userinfo, which is disallowed for cross-origin requests.";
237 bool CrossOriginAccessControl::handleRedirect(SecurityOrigin
* securityOrigin
, ResourceRequest
& newRequest
, const ResourceResponse
& redirectResponse
, StoredCredentials withCredentials
, ResourceLoaderOptions
& options
, String
& errorMessage
)
239 // http://www.w3.org/TR/cors/#redirect-steps terminology:
240 const KURL
& originalURL
= redirectResponse
.url();
241 const KURL
& newURL
= newRequest
.url();
243 bool redirectCrossOrigin
= !securityOrigin
->canRequest(newURL
);
245 // Same-origin request URLs that redirect are allowed without checking access.
246 if (!securityOrigin
->canRequest(originalURL
)) {
247 // Follow http://www.w3.org/TR/cors/#redirect-steps
248 String errorDescription
;
250 // Steps 3 & 4 - check if scheme and other URL restrictions hold.
251 bool allowRedirect
= isLegalRedirectLocation(newURL
, errorDescription
);
253 // Step 5: perform resource sharing access check.
254 allowRedirect
= passesAccessControlCheck(redirectResponse
, withCredentials
, securityOrigin
, errorDescription
, newRequest
.requestContext());
256 RefPtr
<SecurityOrigin
> originalOrigin
= SecurityOrigin::create(originalURL
);
257 // Step 6: if the request URL origin is not same origin as the original URL's,
258 // set the source origin to a globally unique identifier.
259 if (!originalOrigin
->canRequest(newURL
)) {
260 options
.securityOrigin
= SecurityOrigin::createUnique();
261 securityOrigin
= options
.securityOrigin
.get();
265 if (!allowRedirect
) {
266 const String
& originalOrigin
= SecurityOrigin::create(originalURL
)->toString();
267 errorMessage
= "Redirect at origin '" + originalOrigin
+ "' has been blocked from loading by Cross-Origin Resource Sharing policy: " + errorDescription
;
271 if (redirectCrossOrigin
) {
272 // If now to a different origin, update/set Origin:.
273 newRequest
.clearHTTPOrigin();
274 newRequest
.setHTTPOrigin(securityOrigin
->toAtomicString());
275 // If the user didn't request credentials in the first place, update our
276 // state so we neither request them nor expect they must be allowed.
277 if (options
.credentialsRequested
== ClientDidNotRequestCredentials
)
278 options
.allowCredentials
= DoNotAllowStoredCredentials
;