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