blob: 47f16e8377fef35dec725269b3bfa3bc96929381 [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 "platform/loader/fetch/CrossOriginAccessControl.h"
#include <algorithm>
#include <memory>
#include "platform/loader/fetch/FetchUtils.h"
#include "platform/loader/fetch/Resource.h"
#include "platform/loader/fetch/ResourceLoaderOptions.h"
#include "platform/loader/fetch/ResourceRequest.h"
#include "platform/loader/fetch/ResourceResponse.h"
#include "platform/network/HTTPParsers.h"
#include "platform/weborigin/SchemeRegistry.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "platform/wtf/PtrUtil.h"
#include "platform/wtf/Threading.h"
#include "platform/wtf/text/AtomicString.h"
#include "platform/wtf/text/StringBuilder.h"
namespace blink {
bool IsOnAccessControlResponseHeaderWhitelist(const String& name) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(
HTTPHeaderSet, allowed_cross_origin_response_headers,
(new HTTPHeaderSet({
"cache-control", "content-language", "content-type", "expires",
"last-modified", "pragma",
})));
return allowed_cross_origin_response_headers.Contains(name);
}
// Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
static AtomicString CreateAccessControlRequestHeadersHeader(
const HTTPHeaderMap& headers) {
Vector<String> filtered_headers;
for (const auto& header : headers) {
if (FetchUtils::IsSimpleHeader(header.key, header.value)) {
// Exclude simple headers.
continue;
}
if (DeprecatedEqualIgnoringCase(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;
}
filtered_headers.push_back(header.key.DeprecatedLower());
}
if (!filtered_headers.size())
return g_null_atom;
// Sort header names lexicographically.
std::sort(filtered_headers.begin(), filtered_headers.end(),
WTF::CodePointCompareLessThan);
StringBuilder header_buffer;
for (const String& header : filtered_headers) {
if (!header_buffer.IsEmpty())
header_buffer.Append(",");
header_buffer.Append(header);
}
return AtomicString(header_buffer.ToString());
}
ResourceRequest CreateAccessControlPreflightRequest(
const ResourceRequest& request) {
const KURL& request_url = request.Url();
DCHECK(request_url.User().IsEmpty());
DCHECK(request_url.Pass().IsEmpty());
ResourceRequest preflight_request(request_url);
preflight_request.SetAllowStoredCredentials(false);
preflight_request.SetHTTPMethod(HTTPNames::OPTIONS);
preflight_request.SetHTTPHeaderField(HTTPNames::Access_Control_Request_Method,
AtomicString(request.HttpMethod()));
preflight_request.SetPriority(request.Priority());
preflight_request.SetRequestContext(request.GetRequestContext());
preflight_request.SetServiceWorkerMode(
WebURLRequest::ServiceWorkerMode::kNone);
if (request.IsExternalRequest()) {
preflight_request.SetHTTPHeaderField(
HTTPNames::Access_Control_Request_External, "true");
}
AtomicString request_headers =
CreateAccessControlRequestHeadersHeader(request.HttpHeaderFields());
if (request_headers != g_null_atom) {
preflight_request.SetHTTPHeaderField(
HTTPNames::Access_Control_Request_Headers, request_headers);
}
return preflight_request;
}
static bool IsOriginSeparator(UChar ch) {
return IsASCIISpace(ch) || ch == ',';
}
static bool IsInterestingStatusCode(int status_code) {
// Predicate that gates what status codes should be included in console error
// messages for responses containing no access control headers.
return status_code >= 400;
}
static void AppendOriginDeniedMessage(StringBuilder& builder,
const SecurityOrigin* security_origin) {
builder.Append(" Origin '");
builder.Append(security_origin->ToString());
builder.Append("' is therefore not allowed access.");
}
static void AppendNoCORSInformationalMessage(
StringBuilder& builder,
WebURLRequest::RequestContext context) {
if (context != WebURLRequest::kRequestContextFetch)
return;
builder.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.");
}
CrossOriginAccessControl::AccessStatus CrossOriginAccessControl::CheckAccess(
const ResourceResponse& response,
StoredCredentials include_credentials,
const SecurityOrigin* security_origin) {
static const char allow_origin_header_name[] = "access-control-allow-origin";
static const char allow_credentials_header_name[] =
"access-control-allow-credentials";
static const char allow_suborigin_header_name[] =
"access-control-allow-suborigin";
int status_code = response.HttpStatusCode();
if (!status_code)
return kInvalidResponse;
const AtomicString& allow_origin_header_value =
response.HttpHeaderField(allow_origin_header_name);
// Check Suborigins, unless the Access-Control-Allow-Origin is '*', which
// implies that all Suborigins are okay as well.
if (security_origin->HasSuborigin() &&
allow_origin_header_value != g_star_atom) {
const AtomicString& allow_suborigin_header_value =
response.HttpHeaderField(allow_suborigin_header_name);
AtomicString atomic_suborigin_name(
security_origin->GetSuborigin()->GetName());
if (allow_suborigin_header_value != g_star_atom &&
allow_suborigin_header_value != atomic_suborigin_name) {
return kSubOriginMismatch;
}
}
if (allow_origin_header_value == "*") {
// 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 (include_credentials == kDoNotAllowStoredCredentials)
return kAccessAllowed;
if (response.IsHTTP()) {
return kWildcardOriginNotAllowed;
}
} else if (allow_origin_header_value != security_origin->ToAtomicString()) {
if (allow_origin_header_value.IsNull())
return kMissingAllowOriginHeader;
if (allow_origin_header_value.GetString().Find(IsOriginSeparator, 0) !=
kNotFound) {
return kMultipleAllowOriginValues;
}
KURL header_origin(KURL(), allow_origin_header_value);
if (!header_origin.IsValid())
return kInvalidAllowOriginValue;
return kAllowOriginMismatch;
}
if (include_credentials == kAllowStoredCredentials) {
const AtomicString& allow_credentials_header_value =
response.HttpHeaderField(allow_credentials_header_name);
if (allow_credentials_header_value != "true") {
return kDisallowCredentialsNotSetToTrue;
}
}
return kAccessAllowed;
}
void CrossOriginAccessControl::AccessControlErrorString(
StringBuilder& builder,
CrossOriginAccessControl::AccessStatus status,
const ResourceResponse& response,
const SecurityOrigin* security_origin,
WebURLRequest::RequestContext context) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allow_origin_header_name,
(new AtomicString("access-control-allow-origin")));
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allow_credentials_header_name,
(new AtomicString("access-control-allow-credentials")));
DEFINE_THREAD_SAFE_STATIC_LOCAL(
AtomicString, allow_suborigin_header_name,
(new AtomicString("access-control-allow-suborigin")));
switch (status) {
case kInvalidResponse: {
builder.Append("Invalid response.");
AppendOriginDeniedMessage(builder, security_origin);
return;
}
case kSubOriginMismatch: {
const AtomicString& allow_suborigin_header_value =
response.HttpHeaderField(allow_suborigin_header_name);
builder.Append(
"The 'Access-Control-Allow-Suborigin' header has a value '");
builder.Append(allow_suborigin_header_value);
builder.Append("' that is not equal to the supplied suborigin.");
AppendOriginDeniedMessage(builder, security_origin);
return;
}
case kWildcardOriginNotAllowed: {
builder.Append(
"The value of the 'Access-Control-Allow-Origin' header in the "
"response must not be the wildcard '*' when the request's "
"credentials mode is 'include'.");
AppendOriginDeniedMessage(builder, security_origin);
if (context == WebURLRequest::kRequestContextXMLHttpRequest) {
builder.Append(
" The credentials mode of requests initiated by the "
"XMLHttpRequest is controlled by the withCredentials attribute.");
}
return;
}
case kMissingAllowOriginHeader: {
builder.Append(
"No 'Access-Control-Allow-Origin' header is present on the requested "
"resource.");
AppendOriginDeniedMessage(builder, security_origin);
int status_code = response.HttpStatusCode();
if (IsInterestingStatusCode(status_code)) {
builder.Append(" The response had HTTP status code ");
builder.Append(String::Number(status_code));
builder.Append('.');
}
if (context == WebURLRequest::kRequestContextFetch) {
builder.Append(
" If an opaque response serves your needs, set the request's mode "
"to 'no-cors' to fetch the resource with CORS disabled.");
}
return;
}
case kMultipleAllowOriginValues: {
const AtomicString& allow_origin_header_value =
response.HttpHeaderField(allow_origin_header_name);
builder.Append(
"The 'Access-Control-Allow-Origin' header contains multiple values "
"'");
builder.Append(allow_origin_header_value);
builder.Append("', but only one is allowed.");
AppendOriginDeniedMessage(builder, security_origin);
AppendNoCORSInformationalMessage(builder, context);
return;
}
case kInvalidAllowOriginValue: {
const AtomicString& allow_origin_header_value =
response.HttpHeaderField(allow_origin_header_name);
builder.Append(
"The 'Access-Control-Allow-Origin' header contains the invalid "
"value '");
builder.Append(allow_origin_header_value);
builder.Append("'.");
AppendOriginDeniedMessage(builder, security_origin);
AppendNoCORSInformationalMessage(builder, context);
return;
}
case kAllowOriginMismatch: {
const AtomicString& allow_origin_header_value =
response.HttpHeaderField(allow_origin_header_name);
builder.Append("The 'Access-Control-Allow-Origin' header has a value '");
builder.Append(allow_origin_header_value);
builder.Append("' that is not equal to the supplied origin.");
AppendOriginDeniedMessage(builder, security_origin);
AppendNoCORSInformationalMessage(builder, context);
return;
}
case kDisallowCredentialsNotSetToTrue: {
const AtomicString& allow_credentials_header_value =
response.HttpHeaderField(allow_credentials_header_name);
builder.Append(
"The value of the 'Access-Control-Allow-Credentials' header in "
"the response is '");
builder.Append(allow_credentials_header_value);
builder.Append(
"' which must "
"be 'true' when the request's credentials mode is 'include'.");
AppendOriginDeniedMessage(builder, security_origin);
if (context == WebURLRequest::kRequestContextXMLHttpRequest) {
builder.Append(
" The credentials mode of requests initiated by the "
"XMLHttpRequest is controlled by the withCredentials attribute.");
}
return;
}
default:
NOTREACHED();
}
}
CrossOriginAccessControl::PreflightStatus
CrossOriginAccessControl::CheckPreflight(const ResourceResponse& response) {
// 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
int status_code = response.HttpStatusCode();
if (!FetchUtils::IsOkStatus(status_code))
return kPreflightInvalidStatus;
return kPreflightSuccess;
}
CrossOriginAccessControl::PreflightStatus
CrossOriginAccessControl::CheckExternalPreflight(
const ResourceResponse& response) {
AtomicString result =
response.HttpHeaderField(HTTPNames::Access_Control_Allow_External);
if (result.IsNull())
return kPreflightMissingAllowExternal;
if (!DeprecatedEqualIgnoringCase(result, "true"))
return kPreflightInvalidAllowExternal;
return kPreflightSuccess;
}
void CrossOriginAccessControl::PreflightErrorString(
StringBuilder& builder,
CrossOriginAccessControl::PreflightStatus status,
const ResourceResponse& response) {
switch (status) {
case kPreflightInvalidStatus: {
int status_code = response.HttpStatusCode();
builder.Append("Response for preflight has invalid HTTP status code ");
builder.Append(String::Number(status_code));
return;
}
case kPreflightMissingAllowExternal: {
builder.Append(
"No 'Access-Control-Allow-External' header was present in ");
builder.Append(
"the preflight response for this external request (This is");
builder.Append(" an experimental header which is defined in ");
builder.Append("'https://wicg.github.io/cors-rfc1918/').");
return;
}
case kPreflightInvalidAllowExternal: {
String result =
response.HttpHeaderField(HTTPNames::Access_Control_Allow_External);
builder.Append("The 'Access-Control-Allow-External' header in the ");
builder.Append(
"preflight response for this external request had a value");
builder.Append(" of '");
builder.Append(result);
builder.Append("', not 'true' (This is an experimental header which is");
builder.Append(" defined in 'https://wicg.github.io/cors-rfc1918/').");
return;
}
default:
NOTREACHED();
}
}
void ParseAccessControlExposeHeadersAllowList(const String& header_value,
HTTPHeaderSet& header_set) {
Vector<String> headers;
header_value.Split(',', false, headers);
for (unsigned header_count = 0; header_count < headers.size();
header_count++) {
String stripped_header = headers[header_count].StripWhiteSpace();
if (!stripped_header.IsEmpty())
header_set.insert(stripped_header);
}
}
void ExtractCorsExposedHeaderNamesList(const ResourceResponse& response,
HTTPHeaderSet& header_set) {
// 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())
header_set.insert(header);
return;
}
ParseAccessControlExposeHeadersAllowList(
response.HttpHeaderField(HTTPNames::Access_Control_Expose_Headers),
header_set);
}
CrossOriginAccessControl::RedirectStatus
CrossOriginAccessControl::CheckRedirectLocation(const KURL& request_url) {
// 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(
request_url.Protocol()))
return kRedirectDisallowedScheme;
// 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 (!(request_url.User().IsEmpty() && request_url.Pass().IsEmpty()))
return kRedirectContainsCredentials;
return kRedirectSuccess;
}
void CrossOriginAccessControl::RedirectErrorString(
StringBuilder& builder,
CrossOriginAccessControl::RedirectStatus status,
const KURL& request_url) {
switch (status) {
case kRedirectDisallowedScheme: {
builder.Append("Redirect location '");
builder.Append(request_url.GetString());
builder.Append("' has a disallowed scheme for cross-origin requests.");
return;
}
case kRedirectContainsCredentials: {
builder.Append("Redirect location '");
builder.Append(request_url.GetString());
builder.Append(
"' contains a username and password, which is disallowed for"
" cross-origin requests.");
return;
}
default:
NOTREACHED();
}
}
bool CrossOriginAccessControl::HandleRedirect(
RefPtr<SecurityOrigin> current_security_origin,
ResourceRequest& new_request,
const ResourceResponse& redirect_response,
StoredCredentials with_credentials,
ResourceLoaderOptions& options,
String& error_message) {
// http://www.w3.org/TR/cors/#redirect-steps terminology:
const KURL& last_url = redirect_response.Url();
const KURL& new_url = new_request.Url();
RefPtr<SecurityOrigin> new_security_origin = current_security_origin;
// TODO(tyoshino): This should be fixed to check not only the last one but
// all redirect responses.
if (!current_security_origin->CanRequest(last_url)) {
// Follow http://www.w3.org/TR/cors/#redirect-steps
CrossOriginAccessControl::RedirectStatus redirect_status =
CrossOriginAccessControl::CheckRedirectLocation(new_url);
if (redirect_status != kRedirectSuccess) {
StringBuilder builder;
builder.Append("Redirect from '");
builder.Append(last_url.GetString());
builder.Append("' has been blocked by CORS policy: ");
CrossOriginAccessControl::RedirectErrorString(builder, redirect_status,
new_url);
error_message = builder.ToString();
return false;
}
// Step 5: perform resource sharing access check.
CrossOriginAccessControl::AccessStatus cors_status =
CrossOriginAccessControl::CheckAccess(
redirect_response, with_credentials, current_security_origin.Get());
if (cors_status != kAccessAllowed) {
StringBuilder builder;
builder.Append("Redirect from '");
builder.Append(last_url.GetString());
builder.Append("' has been blocked by CORS policy: ");
CrossOriginAccessControl::AccessControlErrorString(
builder, cors_status, redirect_response,
current_security_origin.Get(), new_request.GetRequestContext());
error_message = builder.ToString();
return false;
}
RefPtr<SecurityOrigin> last_origin = SecurityOrigin::Create(last_url);
// 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 (!last_origin->CanRequest(new_url)) {
options.security_origin = SecurityOrigin::CreateUnique();
new_security_origin = options.security_origin;
}
}
if (!current_security_origin->CanRequest(new_url)) {
new_request.ClearHTTPOrigin();
new_request.SetHTTPOrigin(new_security_origin.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.credentials_requested == kClientDidNotRequestCredentials)
options.allow_credentials = kDoNotAllowStoredCredentials;
}
return true;
}
} // namespace blink