Home | History | Annotate | Download | only in fetch
      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 
     43 bool isOnAccessControlSimpleRequestMethodWhitelist(const String& method)
     44 {
     45     return method == "GET" || method == "HEAD" || method == "POST";
     46 }
     47 
     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 
     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 
     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 
     96 bool isOnAccessControlResponseHeaderWhitelist(const String& name)
     97 {
     98     AtomicallyInitializedStatic(HTTPHeaderSet*, allowedCrossOriginResponseHeaders = createAllowedCrossOriginResponseHeadersSet().leakPtr());
     99 
    100     return allowedCrossOriginResponseHeaders->contains(name);
    101 }
    102 
    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 
    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 
    140 static bool isOriginSeparator(UChar ch)
    141 {
    142     return isASCIISpace(ch) || ch == ',';
    143 }
    144 
    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 
    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 
    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 
    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 
    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