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 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.
24 *
25 */
26
27 #include "config.h"
28 #include "core/fetch/CrossOriginAccessControl.h"
29
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"
40
41 namespace blink {
42
createAllowedCrossOriginResponseHeadersSet()43 static PassOwnPtr<HTTPHeaderSet> createAllowedCrossOriginResponseHeadersSet()
44 {
45 OwnPtr<HTTPHeaderSet> headerSet = adoptPtr(new HashSet<String, CaseFoldingHash>);
46
47 headerSet->add("cache-control");
48 headerSet->add("content-language");
49 headerSet->add("content-type");
50 headerSet->add("expires");
51 headerSet->add("last-modified");
52 headerSet->add("pragma");
53
54 return headerSet.release();
55 }
56
isOnAccessControlResponseHeaderWhitelist(const String & name)57 bool isOnAccessControlResponseHeaderWhitelist(const String& name)
58 {
59 AtomicallyInitializedStatic(HTTPHeaderSet*, allowedCrossOriginResponseHeaders = createAllowedCrossOriginResponseHeadersSet().leakPtr());
60
61 return allowedCrossOriginResponseHeaders->contains(name);
62 }
63
updateRequestForAccessControl(ResourceRequest & request,SecurityOrigin * securityOrigin,StoredCredentials allowCredentials)64 void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin* securityOrigin, StoredCredentials allowCredentials)
65 {
66 request.removeCredentials();
67 request.setAllowStoredCredentials(allowCredentials == AllowStoredCredentials);
68
69 if (securityOrigin)
70 request.setHTTPOrigin(securityOrigin->toAtomicString());
71 }
72
createAccessControlPreflightRequest(const ResourceRequest & request,SecurityOrigin * securityOrigin)73 ResourceRequest createAccessControlPreflightRequest(const ResourceRequest& request, SecurityOrigin* securityOrigin)
74 {
75 ResourceRequest preflightRequest(request.url());
76 updateRequestForAccessControl(preflightRequest, securityOrigin, DoNotAllowStoredCredentials);
77 preflightRequest.setHTTPMethod("OPTIONS");
78 preflightRequest.setHTTPHeaderField("Access-Control-Request-Method", request.httpMethod());
79 preflightRequest.setPriority(request.priority());
80 preflightRequest.setRequestContext(request.requestContext());
81
82 const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
83
84 if (requestHeaderFields.size() > 0) {
85 StringBuilder headerBuffer;
86 HTTPHeaderMap::const_iterator it = requestHeaderFields.begin();
87 headerBuffer.append(it->key);
88 ++it;
89
90 HTTPHeaderMap::const_iterator end = requestHeaderFields.end();
91 for (; it != end; ++it) {
92 headerBuffer.appendLiteral(", ");
93 headerBuffer.append(it->key);
94 }
95
96 preflightRequest.setHTTPHeaderField("Access-Control-Request-Headers", AtomicString(headerBuffer.toString().lower()));
97 }
98
99 return preflightRequest;
100 }
101
isOriginSeparator(UChar ch)102 static bool isOriginSeparator(UChar ch)
103 {
104 return isASCIISpace(ch) || ch == ',';
105 }
106
isInterestingStatusCode(int statusCode)107 static bool isInterestingStatusCode(int statusCode)
108 {
109 // Predicate that gates what status codes should be included in
110 // console error messages for responses containing no access
111 // control headers.
112 return statusCode >= 400;
113 }
114
passesAccessControlCheck(const ResourceResponse & response,StoredCredentials includeCredentials,SecurityOrigin * securityOrigin,String & errorDescription)115 bool passesAccessControlCheck(const ResourceResponse& response, StoredCredentials includeCredentials, SecurityOrigin* securityOrigin, String& errorDescription)
116 {
117 AtomicallyInitializedStatic(AtomicString&, accessControlAllowOrigin = *new AtomicString("access-control-allow-origin", AtomicString::ConstructFromLiteral));
118 AtomicallyInitializedStatic(AtomicString&, accessControlAllowCredentials = *new AtomicString("access-control-allow-credentials", AtomicString::ConstructFromLiteral));
119
120 if (!response.httpStatusCode()) {
121 errorDescription = "Received an invalid response. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
122 return false;
123 }
124
125 const AtomicString& accessControlOriginString = response.httpHeaderField(accessControlAllowOrigin);
126 if (accessControlOriginString == starAtom) {
127 // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent,
128 // even with Access-Control-Allow-Credentials set to true.
129 if (includeCredentials == DoNotAllowStoredCredentials)
130 return true;
131 if (response.isHTTP()) {
132 errorDescription = "A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
133 return false;
134 }
135 } else if (accessControlOriginString != securityOrigin->toAtomicString()) {
136 if (accessControlOriginString.isEmpty()) {
137 errorDescription = "No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
138
139 if (isInterestingStatusCode(response.httpStatusCode()))
140 errorDescription.append(" The response had HTTP status code " + String::number(response.httpStatusCode()) + ".");
141 } else if (accessControlOriginString.string().find(isOriginSeparator, 0) != kNotFound) {
142 errorDescription = "The 'Access-Control-Allow-Origin' header contains multiple values '" + accessControlOriginString + "', but only one is allowed. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
143 } else {
144 KURL headerOrigin(KURL(), accessControlOriginString);
145 if (!headerOrigin.isValid())
146 errorDescription = "The 'Access-Control-Allow-Origin' header contains the invalid value '" + accessControlOriginString + "'. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
147 else
148 errorDescription = "The 'Access-Control-Allow-Origin' header has a value '" + accessControlOriginString + "' that is not equal to the supplied origin. Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
149 }
150 return false;
151 }
152
153 if (includeCredentials == AllowStoredCredentials) {
154 const AtomicString& accessControlCredentialsString = response.httpHeaderField(accessControlAllowCredentials);
155 if (accessControlCredentialsString != "true") {
156 errorDescription = "Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is '" + accessControlCredentialsString + "'. It must be 'true' to allow credentials.";
157 return false;
158 }
159 }
160
161 return true;
162 }
163
passesPreflightStatusCheck(const ResourceResponse & response,String & errorDescription)164 bool passesPreflightStatusCheck(const ResourceResponse& response, String& errorDescription)
165 {
166 if (response.httpStatusCode() < 200 || response.httpStatusCode() >= 400) {
167 errorDescription = "Invalid HTTP status code " + String::number(response.httpStatusCode());
168 return false;
169 }
170
171 return true;
172 }
173
parseAccessControlExposeHeadersAllowList(const String & headerValue,HTTPHeaderSet & headerSet)174 void parseAccessControlExposeHeadersAllowList(const String& headerValue, HTTPHeaderSet& headerSet)
175 {
176 Vector<String> headers;
177 headerValue.split(',', false, headers);
178 for (unsigned headerCount = 0; headerCount < headers.size(); headerCount++) {
179 String strippedHeader = headers[headerCount].stripWhiteSpace();
180 if (!strippedHeader.isEmpty())
181 headerSet.add(strippedHeader);
182 }
183 }
184
isLegalRedirectLocation(const KURL & requestURL,String & errorDescription)185 bool CrossOriginAccessControl::isLegalRedirectLocation(const KURL& requestURL, String& errorDescription)
186 {
187 // CORS restrictions imposed on Location: URL -- http://www.w3.org/TR/cors/#redirect-steps (steps 2 + 3.)
188 if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL.protocol())) {
189 errorDescription = "The request was redirected to a URL ('" + requestURL.string() + "') which has a disallowed scheme for cross-origin requests.";
190 return false;
191 }
192
193 if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty())) {
194 errorDescription = "The request was redirected to a URL ('" + requestURL.string() + "') containing userinfo, which is disallowed for cross-origin requests.";
195 return false;
196 }
197
198 return true;
199 }
200
handleRedirect(Resource * resource,SecurityOrigin * securityOrigin,ResourceRequest & request,const ResourceResponse & redirectResponse,ResourceLoaderOptions & options,String & errorMessage)201 bool CrossOriginAccessControl::handleRedirect(Resource* resource, SecurityOrigin* securityOrigin, ResourceRequest& request, const ResourceResponse& redirectResponse, ResourceLoaderOptions& options, String& errorMessage)
202 {
203 // http://www.w3.org/TR/cors/#redirect-steps terminology:
204 const KURL& originalURL = redirectResponse.url();
205 const KURL& requestURL = request.url();
206
207 bool redirectCrossOrigin = !securityOrigin->canRequest(requestURL);
208
209 // Same-origin request URLs that redirect are allowed without checking access.
210 if (!securityOrigin->canRequest(originalURL)) {
211 // Follow http://www.w3.org/TR/cors/#redirect-steps
212 String errorDescription;
213
214 // Steps 3 & 4 - check if scheme and other URL restrictions hold.
215 bool allowRedirect = isLegalRedirectLocation(requestURL, errorDescription);
216 if (allowRedirect) {
217 // Step 5: perform resource sharing access check.
218 StoredCredentials withCredentials = resource->lastResourceRequest().allowStoredCredentials() ? AllowStoredCredentials : DoNotAllowStoredCredentials;
219 allowRedirect = passesAccessControlCheck(redirectResponse, withCredentials, securityOrigin, errorDescription);
220 if (allowRedirect) {
221 RefPtr<SecurityOrigin> originalOrigin = SecurityOrigin::create(originalURL);
222 // Step 6: if the request URL origin is not same origin as the original URL's,
223 // set the source origin to a globally unique identifier.
224 if (!originalOrigin->canRequest(requestURL)) {
225 options.securityOrigin = SecurityOrigin::createUnique();
226 securityOrigin = options.securityOrigin.get();
227 }
228 }
229 }
230 if (!allowRedirect) {
231 const String& originalOrigin = SecurityOrigin::create(originalURL)->toString();
232 errorMessage = "Redirect at origin '" + originalOrigin + "' has been blocked from loading by Cross-Origin Resource Sharing policy: " + errorDescription;
233 return false;
234 }
235 }
236 if (redirectCrossOrigin) {
237 // If now to a different origin, update/set Origin:.
238 request.clearHTTPOrigin();
239 request.setHTTPOrigin(securityOrigin->toAtomicString());
240 // If the user didn't request credentials in the first place, update our
241 // state so we neither request them nor expect they must be allowed.
242 if (options.credentialsRequested == ClientDidNotRequestCredentials)
243 options.allowCredentials = DoNotAllowStoredCredentials;
244 }
245 return true;
246 }
247
248 } // namespace blink
249