blob: facfe26710166dfc10dc6b3ed726201528625284 [file] [log] [blame]
/*
* Copyright (C) 2008 Apple Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#include "config.h"
#include "core/fetch/CrossOriginAccessControl.h"
#include "core/fetch/Resource.h"
#include "core/fetch/ResourceLoaderOptions.h"
#include "platform/network/HTTPParsers.h"
#include "platform/network/ResourceRequest.h"
#include "platform/network/ResourceResponse.h"
#include "platform/weborigin/SchemeRegistry.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "wtf/Threading.h"
#include "wtf/text/AtomicString.h"
#include "wtf/text/StringBuilder.h"
#include <algorithm>
namespace blink {
static PassOwnPtr<HTTPHeaderSet> createAllowedCrossOriginResponseHeadersSet()
{
OwnPtr<HTTPHeaderSet> headerSet = adoptPtr(new HashSet<String, CaseFoldingHash>);
headerSet->add("cache-control");
headerSet->add("content-language");
headerSet->add("content-type");
headerSet->add("expires");
headerSet->add("last-modified");
headerSet->add("pragma");
return headerSet.release();
}
bool isOnAccessControlResponseHeaderWhitelist(const String& name)
{
AtomicallyInitializedStaticReference(HTTPHeaderSet, allowedCrossOriginResponseHeaders, (createAllowedCrossOriginResponseHeadersSet().leakPtr()));
return allowedCrossOriginResponseHeaders.contains(name);
}
void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin* securityOrigin, StoredCredentials allowCredentials)
{
request.removeCredentials();
request.setAllowStoredCredentials(allowCredentials == AllowStoredCredentials);
request.setFetchCredentialsMode(allowCredentials == AllowStoredCredentials ? WebURLRequest::FetchCredentialsModeInclude : WebURLRequest::FetchCredentialsModeOmit);
if (securityOrigin)
request.setHTTPOrigin(securityOrigin->toAtomicString());
}
ResourceRequest createAccessControlPreflightRequest(const ResourceRequest& request, SecurityOrigin* securityOrigin)
{
ResourceRequest preflightRequest(request.url());
updateRequestForAccessControl(preflightRequest, securityOrigin, DoNotAllowStoredCredentials);
preflightRequest.setHTTPMethod("OPTIONS");
preflightRequest.setHTTPHeaderField("Access-Control-Request-Method", request.httpMethod());
preflightRequest.setPriority(request.priority());
preflightRequest.setRequestContext(request.requestContext());
preflightRequest.setSkipServiceWorker(true);
const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
if (requestHeaderFields.size() > 0) {
// Sort header names lexicographically: https://crbug.com/452391
// Fetch API Spec:
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
Vector<String> headers;
for (const auto& header : requestHeaderFields) {
if (equalIgnoringCase(header.key, "referer")) {
// When the request is from a Worker, referrer header was added
// by WorkerThreadableLoader. But it should not be added to
// Access-Control-Request-Headers header.
continue;
}
headers.append(header.key.lower());
}
std::sort(headers.begin(), headers.end(), WTF::codePointCompareLessThan);
StringBuilder headerBuffer;
for (const String& header : headers) {
if (!headerBuffer.isEmpty())
headerBuffer.appendLiteral(", ");
headerBuffer.append(header);
}
preflightRequest.setHTTPHeaderField("Access-Control-Request-Headers", AtomicString(headerBuffer.toString()));
}
return preflightRequest;
}
static bool isOriginSeparator(UChar ch)
{
return isASCIISpace(ch) || ch == ',';
}
static bool isInterestingStatusCode(int statusCode)
{
// Predicate that gates what status codes should be included in
// console error messages for responses containing no access
// control headers.
return statusCode >= 400;
}
static String buildAccessControlFailureMessage(const String& detail, SecurityOrigin* securityOrigin)
{
return detail + " Origin '" + securityOrigin->toString() + "' is therefore not allowed access.";
}
bool passesAccessControlCheck(const ResourceResponse& response, StoredCredentials includeCredentials, SecurityOrigin* securityOrigin, String& errorDescription)
{
AtomicallyInitializedStaticReference(AtomicString, allowOriginHeaderName, (new AtomicString("access-control-allow-origin", AtomicString::ConstructFromLiteral)));
AtomicallyInitializedStaticReference(AtomicString, allowCredentialsHeaderName, (new AtomicString("access-control-allow-credentials", AtomicString::ConstructFromLiteral)));
int statusCode = response.httpStatusCode();
if (!statusCode) {
errorDescription = buildAccessControlFailureMessage("Invalid response.", securityOrigin);
return false;
}
const AtomicString& allowOriginHeaderValue = response.httpHeaderField(allowOriginHeaderName);
if (allowOriginHeaderValue == starAtom) {
// A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent,
// even with Access-Control-Allow-Credentials set to true.
if (includeCredentials == DoNotAllowStoredCredentials)
return true;
if (response.isHTTP()) {
errorDescription = buildAccessControlFailureMessage("A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true.", securityOrigin);
return false;
}
} else if (allowOriginHeaderValue != securityOrigin->toAtomicString()) {
if (allowOriginHeaderValue.isNull()) {
errorDescription = buildAccessControlFailureMessage("No 'Access-Control-Allow-Origin' header is present on the requested resource.", securityOrigin);
if (isInterestingStatusCode(statusCode))
errorDescription.append(" The response had HTTP status code " + String::number(statusCode) + ".");
return false;
}
String detail;
if (allowOriginHeaderValue.string().find(isOriginSeparator, 0) != kNotFound) {
detail = "The 'Access-Control-Allow-Origin' header contains multiple values '" + allowOriginHeaderValue + "', but only one is allowed.";
} else {
KURL headerOrigin(KURL(), allowOriginHeaderValue);
if (!headerOrigin.isValid())
detail = "The 'Access-Control-Allow-Origin' header contains the invalid value '" + allowOriginHeaderValue + "'.";
else
detail = "The 'Access-Control-Allow-Origin' header has a value '" + allowOriginHeaderValue + "' that is not equal to the supplied origin.";
}
errorDescription = buildAccessControlFailureMessage(detail, securityOrigin);
return false;
}
if (includeCredentials == AllowStoredCredentials) {
const AtomicString& allowCredentialsHeaderValue = response.httpHeaderField(allowCredentialsHeaderName);
if (allowCredentialsHeaderValue != "true") {
errorDescription = buildAccessControlFailureMessage("Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is '" + allowCredentialsHeaderValue + "'. It must be 'true' to allow credentials.", securityOrigin);
return false;
}
}
return true;
}
bool passesPreflightStatusCheck(const ResourceResponse& response, String& errorDescription)
{
// CORS preflight with 3XX is considered network error in
// Fetch API Spec:
// https://fetch.spec.whatwg.org/#cors-preflight-fetch
// CORS Spec:
// http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
// https://crbug.com/452394
if (response.httpStatusCode() < 200 || response.httpStatusCode() >= 300) {
errorDescription = "Response for preflight has invalid HTTP status code " + String::number(response.httpStatusCode());
return false;
}
return true;
}
void parseAccessControlExposeHeadersAllowList(const String& headerValue, HTTPHeaderSet& headerSet)
{
Vector<String> headers;
headerValue.split(',', false, headers);
for (unsigned headerCount = 0; headerCount < headers.size(); headerCount++) {
String strippedHeader = headers[headerCount].stripWhiteSpace();
if (!strippedHeader.isEmpty())
headerSet.add(strippedHeader);
}
}
bool CrossOriginAccessControl::isLegalRedirectLocation(const KURL& requestURL, String& errorDescription)
{
// CORS restrictions imposed on Location: URL -- http://www.w3.org/TR/cors/#redirect-steps (steps 2 + 3.)
if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL.protocol())) {
errorDescription = "The request was redirected to a URL ('" + requestURL.string() + "') which has a disallowed scheme for cross-origin requests.";
return false;
}
if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty())) {
errorDescription = "The request was redirected to a URL ('" + requestURL.string() + "') containing userinfo, which is disallowed for cross-origin requests.";
return false;
}
return true;
}
bool CrossOriginAccessControl::handleRedirect(SecurityOrigin* securityOrigin, ResourceRequest& newRequest, const ResourceResponse& redirectResponse, StoredCredentials withCredentials, ResourceLoaderOptions& options, String& errorMessage)
{
// http://www.w3.org/TR/cors/#redirect-steps terminology:
const KURL& originalURL = redirectResponse.url();
const KURL& newURL = newRequest.url();
bool redirectCrossOrigin = !securityOrigin->canRequest(newURL);
// Same-origin request URLs that redirect are allowed without checking access.
if (!securityOrigin->canRequest(originalURL)) {
// Follow http://www.w3.org/TR/cors/#redirect-steps
String errorDescription;
// Steps 3 & 4 - check if scheme and other URL restrictions hold.
bool allowRedirect = isLegalRedirectLocation(newURL, errorDescription);
if (allowRedirect) {
// Step 5: perform resource sharing access check.
allowRedirect = passesAccessControlCheck(redirectResponse, withCredentials, securityOrigin, errorDescription);
if (allowRedirect) {
RefPtr<SecurityOrigin> originalOrigin = SecurityOrigin::create(originalURL);
// Step 6: if the request URL origin is not same origin as the original URL's,
// set the source origin to a globally unique identifier.
if (!originalOrigin->canRequest(newURL)) {
options.securityOrigin = SecurityOrigin::createUnique();
securityOrigin = options.securityOrigin.get();
}
}
}
if (!allowRedirect) {
const String& originalOrigin = SecurityOrigin::create(originalURL)->toString();
errorMessage = "Redirect at origin '" + originalOrigin + "' has been blocked from loading by Cross-Origin Resource Sharing policy: " + errorDescription;
return false;
}
}
if (redirectCrossOrigin) {
// If now to a different origin, update/set Origin:.
newRequest.clearHTTPOrigin();
newRequest.setHTTPOrigin(securityOrigin->toAtomicString());
// If the user didn't request credentials in the first place, update our
// state so we neither request them nor expect they must be allowed.
if (options.credentialsRequested == ClientDidNotRequestCredentials)
options.allowCredentials = DoNotAllowStoredCredentials;
}
return true;
}
} // namespace blink