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
41namespace WebCore {
42
43bool isOnAccessControlSimpleRequestMethodWhitelist(const String& method)
44{
45    return method == "GET" || method == "HEAD" || method == "POST";
46}
47
48bool 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
68bool 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
82static 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
96bool isOnAccessControlResponseHeaderWhitelist(const String& name)
97{
98    AtomicallyInitializedStatic(HTTPHeaderSet*, allowedCrossOriginResponseHeaders = createAllowedCrossOriginResponseHeadersSet().leakPtr());
99
100    return allowedCrossOriginResponseHeaders->contains(name);
101}
102
103void 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
112ResourceRequest 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
140static bool isOriginSeparator(UChar ch)
141{
142    return isASCIISpace(ch) || ch == ',';
143}
144
145bool 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
191bool 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
201void 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
212bool 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
228bool 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