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 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 57 bool isOnAccessControlResponseHeaderWhitelist(const String& name) 58 { 59 AtomicallyInitializedStatic(HTTPHeaderSet*, allowedCrossOriginResponseHeaders = createAllowedCrossOriginResponseHeadersSet().leakPtr()); 60 61 return allowedCrossOriginResponseHeaders->contains(name); 62 } 63 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 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 102 static bool isOriginSeparator(UChar ch) 103 { 104 return isASCIISpace(ch) || ch == ','; 105 } 106 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 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 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 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 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 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