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