blob: adf384456cbde047776266ec431a9c39fe6c78d2 [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 "core/fetch/CrossOriginAccessControl.h"
#include "core/fetch/FetchUtils.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/PtrUtil.h"
#include "wtf/Threading.h"
#include "wtf/text/AtomicString.h"
#include "wtf/text/StringBuilder.h"
#include <algorithm>
#include <memory>
namespace blink {
bool isOnAccessControlResponseHeaderWhitelist(const String& name) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(
HTTPHeaderSet, allowedCrossOriginResponseHeaders,
(new HTTPHeaderSet({
"cache-control", "content-language", "content-type", "expires",
"last-modified", "pragma",
})));
return allowedCrossOriginResponseHeaders.contains(name);
}
void updateRequestForAccessControl(ResourceRequest& request,
const SecurityOrigin* securityOrigin,
StoredCredentials allowCredentials) {
request.removeCredentials();
request.setAllowStoredCredentials(allowCredentials == AllowStoredCredentials);
if (securityOrigin)
request.setHTTPOrigin(securityOrigin);
}
ResourceRequest createAccessControlPreflightRequest(
const ResourceRequest& request,
const SecurityOrigin* securityOrigin) {
ResourceRequest preflightRequest(request.url());
updateRequestForAccessControl(preflightRequest, securityOrigin,
DoNotAllowStoredCredentials);
preflightRequest.setHTTPMethod(HTTPNames::OPTIONS);
preflightRequest.setHTTPHeaderField(HTTPNames::Access_Control_Request_Method,
AtomicString(request.httpMethod()));
preflightRequest.setPriority(request.priority());
preflightRequest.setRequestContext(request.requestContext());
preflightRequest.setSkipServiceWorker(WebURLRequest::SkipServiceWorker::All);
if (request.isExternalRequest())
preflightRequest.setHTTPHeaderField(
HTTPNames::Access_Control_Request_External, "true");
const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
if (requestHeaderFields.size() > 0) {
// Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
Vector<String> headers;
for (const auto& header : requestHeaderFields) {
if (FetchUtils::isSimpleHeader(header.key, header.value)) {
// Exclude simple headers.
continue;
}
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());
}
// Sort header names lexicographically.
std::sort(headers.begin(), headers.end(), WTF::codePointCompareLessThan);
StringBuilder headerBuffer;
for (const String& header : headers) {
if (!headerBuffer.isEmpty())
headerBuffer.append(", ");
headerBuffer.append(header);
}
preflightRequest.setHTTPHeaderField(
HTTPNames::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,
const SecurityOrigin* securityOrigin) {
return detail + " Origin '" + securityOrigin->toString() +
"' is therefore not allowed access.";
}
bool passesAccessControlCheck(const ResourceResponse& response,
StoredCredentials includeCredentials,
const SecurityOrigin* securityOrigin,
String& errorDescription,
WebURLRequest::RequestContext context) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allowOriginHeaderName,
(new AtomicString("access-control-allow-origin")));
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allowCredentialsHeaderName,
(new AtomicString("access-control-allow-credentials")));
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allowSuboriginHeaderName,
(new AtomicString("access-control-allow-suborigin")));
// TODO(esprehn): This code is using String::append extremely inefficiently
// causing tons of copies. It should pass around a StringBuilder instead.
int statusCode = response.httpStatusCode();
if (!statusCode) {
errorDescription =
buildAccessControlFailureMessage("Invalid response.", securityOrigin);
return false;
}
const AtomicString& allowOriginHeaderValue =
response.httpHeaderField(allowOriginHeaderName);
// Check Suborigins, unless the Access-Control-Allow-Origin is '*', which
// implies that all Suborigins are okay as well.
if (securityOrigin->hasSuborigin() && allowOriginHeaderValue != starAtom) {
const AtomicString& allowSuboriginHeaderValue =
response.httpHeaderField(allowSuboriginHeaderName);
AtomicString atomicSuboriginName(securityOrigin->suborigin()->name());
if (allowSuboriginHeaderValue != starAtom &&
allowSuboriginHeaderValue != atomicSuboriginName) {
errorDescription = buildAccessControlFailureMessage(
"The 'Access-Control-Allow-Suborigin' header has a value '" +
allowSuboriginHeaderValue +
"' that is not equal to the supplied suborigin.",
securityOrigin);
return false;
}
}
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);
if (context == WebURLRequest::RequestContextXMLHttpRequest)
errorDescription.append(
" The credentials mode of an XMLHttpRequest is controlled by the "
"withCredentials attribute.");
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 ");
errorDescription.append(String::number(statusCode));
errorDescription.append('.');
}
if (context == WebURLRequest::RequestContextFetch)
errorDescription.append(
" If an opaque response serves your needs, set the request's mode "
"to 'no-cors' to fetch the resource with CORS disabled.");
return false;
}
String detail;
if (allowOriginHeaderValue.getString().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);
if (context == WebURLRequest::RequestContextFetch)
errorDescription.append(
" Have the server send the header with a valid value, or, if an "
"opaque response serves your needs, set the request's mode to "
"'no-cors' to fetch the resource with CORS disabled.");
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;
}
bool passesExternalPreflightCheck(const ResourceResponse& response,
String& errorDescription) {
AtomicString result =
response.httpHeaderField(HTTPNames::Access_Control_Allow_External);
if (result.isNull()) {
errorDescription =
"No 'Access-Control-Allow-External' header was present in the "
"preflight response for this external request (This is an experimental "
"header which is defined in "
"'https://mikewest.github.io/cors-rfc1918/').";
return false;
}
if (!equalIgnoringCase(result, "true")) {
errorDescription =
"The 'Access-Control-Allow-External' header in the preflight response "
"for this external request had a value of '" +
result +
"', not 'true' (This is an experimental header which is defined in "
"'https://mikewest.github.io/cors-rfc1918/').";
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);
}
}
void extractCorsExposedHeaderNamesList(const ResourceResponse& response,
HTTPHeaderSet& headerSet) {
// If a response was fetched via a service worker, it will always have
// corsExposedHeaderNames set, either from the Access-Control-Expose-Headers
// header, or explicitly via foreign fetch. For requests that didn't come from
// a service worker, foreign fetch doesn't apply so just parse the CORS
// header.
if (response.wasFetchedViaServiceWorker()) {
for (const auto& header : response.corsExposedHeaderNames())
headerSet.add(header);
return;
}
parseAccessControlExposeHeadersAllowList(
response.httpHeaderField(HTTPNames::Access_Control_Expose_Headers),
headerSet);
}
bool CrossOriginAccessControl::isLegalRedirectLocation(
const KURL& requestURL,
String& errorDescription) {
// Block non HTTP(S) schemes as specified in the step 4 in
// https://fetch.spec.whatwg.org/#http-redirect-fetch. Chromium also allows
// the data scheme.
//
// TODO(tyoshino): This check should be performed regardless of the CORS flag
// and request's mode.
if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(
requestURL.protocol())) {
errorDescription = "Redirect location '" + requestURL.getString() +
"' has a disallowed scheme for cross-origin requests.";
return false;
}
// Block URLs including credentials as specified in the step 9 in
// https://fetch.spec.whatwg.org/#http-redirect-fetch.
//
// TODO(tyoshino): This check should be performed also when request's
// origin is not same origin with the redirect destination's origin.
if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty())) {
errorDescription =
"Redirect location '" + requestURL.getString() +
"' contains userinfo, which is disallowed for cross-origin requests.";
return false;
}
return true;
}
bool CrossOriginAccessControl::handleRedirect(
PassRefPtr<SecurityOrigin> securityOrigin,
ResourceRequest& newRequest,
const ResourceResponse& redirectResponse,
StoredCredentials withCredentials,
ResourceLoaderOptions& options,
String& errorMessage) {
// http://www.w3.org/TR/cors/#redirect-steps terminology:
const KURL& lastURL = redirectResponse.url();
const KURL& newURL = newRequest.url();
RefPtr<SecurityOrigin> currentSecurityOrigin = securityOrigin;
RefPtr<SecurityOrigin> newSecurityOrigin = currentSecurityOrigin;
// TODO(tyoshino): This should be fixed to check not only the last one but
// all redirect responses.
if (!currentSecurityOrigin->canRequest(lastURL)) {
// Follow http://www.w3.org/TR/cors/#redirect-steps
String errorDescription;
if (!isLegalRedirectLocation(newURL, errorDescription)) {
errorMessage = "Redirect from '" + lastURL.getString() +
"' has been blocked by CORS policy: " + errorDescription;
return false;
}
// Step 5: perform resource sharing access check.
if (!passesAccessControlCheck(redirectResponse, withCredentials,
currentSecurityOrigin.get(), errorDescription,
newRequest.requestContext())) {
errorMessage = "Redirect from '" + lastURL.getString() +
"' has been blocked by CORS policy: " + errorDescription;
return false;
}
RefPtr<SecurityOrigin> lastOrigin = SecurityOrigin::create(lastURL);
// Set request's origin to a globally unique identifier as specified in
// the step 10 in https://fetch.spec.whatwg.org/#http-redirect-fetch.
if (!lastOrigin->canRequest(newURL)) {
options.securityOrigin = SecurityOrigin::createUnique();
newSecurityOrigin = options.securityOrigin;
}
}
if (!currentSecurityOrigin->canRequest(newURL)) {
newRequest.clearHTTPOrigin();
newRequest.setHTTPOrigin(newSecurityOrigin.get());
// Unset credentials flag if request's credentials mode is "same-origin" as
// request's response tainting becomes "cors".
//
// This is equivalent to the step 2 in
// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
if (options.credentialsRequested == ClientDidNotRequestCredentials)
options.allowCredentials = DoNotAllowStoredCredentials;
}
return true;
}
} // namespace blink