blob: fa4586e30348ca0ff427cde7b68ff3c25f2bce20 [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 "public/platform/WebCORS.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "net/http/http_util.h"
#include "platform/loader/fetch/FetchUtils.h"
#include "platform/loader/fetch/ResourceLoaderOptions.h"
#include "platform/loader/fetch/ResourceRequest.h"
#include "platform/loader/fetch/ResourceResponse.h"
#include "platform/network/http_names.h"
#include "platform/weborigin/KURL.h"
#include "platform/weborigin/SchemeRegistry.h"
#include "platform/wtf/text/StringBuilder.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/WebSecurityOrigin.h"
#include "public/platform/WebURLResponse.h"
#include "url/gurl.h"
#include "url/url_util.h"
using network::mojom::CORSError;
namespace blink {
namespace WebCORS {
namespace {
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;
}
// Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
String CreateAccessControlRequestHeadersHeader(
const WebHTTPHeaderMap& headers) {
Vector<String> filtered_headers;
for (const auto& header : headers.GetHTTPHeaderMap()) {
if (FetchUtils::IsCORSSafelistedHeader(header.key, header.value)) {
// Exclude CORS-safelisted headers.
continue;
}
// TODO(hintzed) replace with EqualIgnoringASCIICase()
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 header_buffer.ToString();
}
// A parser for the value of the Access-Control-Expose-Headers header.
class HTTPHeaderNameListParser {
STACK_ALLOCATED();
public:
explicit HTTPHeaderNameListParser(const String& value)
: value_(value), pos_(0) {}
// Tries parsing |value_| expecting it to be conforming to the #field-name
// ABNF rule defined in RFC 7230. Returns with the field-name entries stored
// in |output| when successful. Otherwise, returns with |output| kept empty.
//
// |output| must be empty.
void Parse(WebHTTPHeaderSet& output) {
DCHECK(output.empty());
while (true) {
ConsumeSpaces();
size_t token_start = pos_;
ConsumeTokenChars();
size_t token_size = pos_ - token_start;
if (token_size == 0) {
output.clear();
return;
}
const CString& name = value_.Substring(token_start, token_size).Ascii();
output.emplace(name.data(), name.length());
ConsumeSpaces();
if (pos_ == value_.length())
return;
if (value_[pos_] == ',') {
++pos_;
} else {
output.clear();
return;
}
}
}
private:
// Consumes zero or more spaces (SP and HTAB) from value_.
void ConsumeSpaces() {
while (true) {
if (pos_ == value_.length())
return;
UChar c = value_[pos_];
if (c != ' ' && c != '\t')
return;
++pos_;
}
}
// Consumes zero or more tchars from value_.
void ConsumeTokenChars() {
while (true) {
if (pos_ == value_.length())
return;
UChar c = value_[pos_];
if (c > 0x7F || !net::HttpUtil::IsTokenChar(c))
return;
++pos_;
}
}
const String value_;
size_t pos_;
};
static bool IsOriginSeparator(UChar ch) {
return IsASCIISpace(ch) || ch == ',';
}
} // namespace
base::Optional<CORSError> CheckAccess(
const WebURL response_url,
const int response_status_code,
const WebHTTPHeaderMap& response_header,
const network::mojom::FetchCredentialsMode credentials_mode,
const WebSecurityOrigin& security_origin) {
if (!response_status_code)
return CORSError::kInvalidResponse;
const WebString& allow_origin_header_value =
response_header.Get(HTTPNames::Access_Control_Allow_Origin);
// Check Suborigins, unless the Access-Control-Allow-Origin is '*', which
// implies that all Suborigins are okay as well.
if (!security_origin.Suborigin().IsEmpty() &&
allow_origin_header_value != WebString(g_star_atom)) {
const WebString& allow_suborigin_header_value =
response_header.Get(HTTPNames::Access_Control_Allow_Suborigin);
if (allow_suborigin_header_value != WebString(g_star_atom) &&
allow_suborigin_header_value != security_origin.Suborigin()) {
return CORSError::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 (credentials_mode != network::mojom::FetchCredentialsMode::kInclude)
return base::nullopt;
// TODO(hintzed): Is the following a sound substitute for
// blink::ResourceResponse::IsHTTP()?
if (GURL(response_url.GetString().Utf16()).SchemeIsHTTPOrHTTPS())
return CORSError::kWildcardOriginNotAllowed;
} else if (allow_origin_header_value != security_origin.ToString()) {
if (allow_origin_header_value.IsNull())
return CORSError::kMissingAllowOriginHeader;
if (String(allow_origin_header_value).Find(IsOriginSeparator, 0) !=
kNotFound) {
return CORSError::kMultipleAllowOriginValues;
}
KURL header_origin(NullURL(), allow_origin_header_value);
if (!header_origin.IsValid())
return CORSError::kInvalidAllowOriginValue;
return CORSError::kAllowOriginMismatch;
}
if (credentials_mode == network::mojom::FetchCredentialsMode::kInclude) {
const WebString& allow_credentials_header_value =
response_header.Get(HTTPNames::Access_Control_Allow_Credentials);
if (allow_credentials_header_value != "true")
return CORSError::kDisallowCredentialsNotSetToTrue;
}
return base::nullopt;
}
base::Optional<CORSError> HandleRedirect(
WebSecurityOrigin& current_security_origin,
WebURLRequest& new_request,
const WebURL redirect_response_url,
const int redirect_response_status_code,
const WebHTTPHeaderMap& redirect_response_header,
network::mojom::FetchCredentialsMode credentials_mode,
ResourceLoaderOptions& options) {
const KURL& last_url = redirect_response_url;
const KURL& new_url = new_request.Url();
WebSecurityOrigin& 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)) {
base::Optional<CORSError> redirect_error = CheckRedirectLocation(new_url);
if (redirect_error)
return redirect_error;
base::Optional<CORSError> access_error = CheckAccess(
redirect_response_url, redirect_response_status_code,
redirect_response_header, credentials_mode, current_security_origin);
if (access_error)
return access_error;
scoped_refptr<const 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.ClearHTTPHeaderField(WebString(HTTPNames::Suborigin));
new_request.SetHTTPHeaderField(WebString(HTTPNames::Origin),
new_security_origin.ToString());
if (!new_security_origin.Suborigin().IsEmpty()) {
new_request.SetHTTPHeaderField(WebString(HTTPNames::Suborigin),
new_security_origin.Suborigin());
}
options.cors_flag = true;
}
return base::nullopt;
}
base::Optional<CORSError> CheckRedirectLocation(const WebURL& web_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.
KURL request_url = web_request_url;
if (!SchemeRegistry::ShouldTreatURLSchemeAsCORSEnabled(
request_url.Protocol())) {
return CORSError::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 CORSError::kRedirectContainsCredentials;
return base::nullopt;
}
base::Optional<CORSError> CheckPreflight(
const int preflight_response_status_code) {
// 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 (!FetchUtils::IsOkStatus(preflight_response_status_code))
return CORSError::kPreflightInvalidStatus;
return base::nullopt;
}
base::Optional<CORSError> CheckExternalPreflight(
const WebHTTPHeaderMap& response_header) {
WebString result =
response_header.Get(HTTPNames::Access_Control_Allow_External);
if (result.IsNull())
return CORSError::kPreflightMissingAllowExternal;
if (!EqualIgnoringASCIICase(result, "true"))
return CORSError::kPreflightInvalidAllowExternal;
return base::nullopt;
}
WebURLRequest CreateAccessControlPreflightRequest(
const WebURLRequest& request) {
const KURL& request_url = request.Url();
DCHECK(request_url.User().IsEmpty());
DCHECK(request_url.Pass().IsEmpty());
WebURLRequest preflight_request(request_url);
preflight_request.SetHTTPMethod(HTTPNames::OPTIONS);
preflight_request.SetHTTPHeaderField(HTTPNames::Access_Control_Request_Method,
request.HttpMethod());
preflight_request.SetPriority(request.GetPriority());
preflight_request.SetRequestContext(request.GetRequestContext());
preflight_request.SetFetchCredentialsMode(
network::mojom::FetchCredentialsMode::kOmit);
preflight_request.SetServiceWorkerMode(
WebURLRequest::ServiceWorkerMode::kNone);
preflight_request.SetHTTPReferrer(request.HttpHeaderField(HTTPNames::Referer),
request.GetReferrerPolicy());
if (request.IsExternalRequest()) {
preflight_request.SetHTTPHeaderField(
HTTPNames::Access_Control_Request_External, "true");
}
String request_headers = CreateAccessControlRequestHeadersHeader(
request.ToResourceRequest().HttpHeaderFields());
if (request_headers != g_null_atom) {
preflight_request.SetHTTPHeaderField(
HTTPNames::Access_Control_Request_Headers, request_headers);
}
return preflight_request;
}
WebString GetErrorString(const CORSError error,
const WebURL& request_url,
const WebURL& redirect_url,
const int response_status_code,
const WebHTTPHeaderMap& response_header,
const WebSecurityOrigin& origin,
const WebURLRequest::RequestContext context) {
static const char kNoCorsInformation[] =
" 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.";
String redirect_denied =
redirect_url.IsValid()
? String::Format(
"Redirect from '%s' to '%s' has been blocked by CORS policy: ",
request_url.GetString().Utf8().data(),
redirect_url.GetString().Utf8().data())
: String();
switch (error) {
case CORSError::kDisallowedByMode:
return String::Format(
"Failed to load '%s': Cross origin requests are not allowed by "
"request mode.",
request_url.GetString().Utf8().data());
case CORSError::kInvalidResponse:
return String::Format(
"%sInvalid response. Origin '%s' is therefore not allowed access.",
redirect_denied.Utf8().data(), origin.ToString().Utf8().data());
case CORSError::kSubOriginMismatch:
return String::Format(
"%sThe 'Access-Control-Allow-Suborigin' header has a value '%s' that "
"is not equal to the supplied suborigin. Origin '%s' is therefore "
"not allowed access.",
redirect_denied.Utf8().data(),
response_header.Get(HTTPNames::Access_Control_Allow_Suborigin)
.Utf8()
.data(),
origin.ToString().Utf8().data());
case CORSError::kWildcardOriginNotAllowed:
return String::Format(
"%sThe value of the 'Access-Control-Allow-Origin' header in the "
"response must not be the wildcard '*' when the request's "
"credentials mode is 'include'. Origin '%s' is therefore not allowed "
"access.%s",
redirect_denied.Utf8().data(), origin.ToString().Utf8().data(),
context == WebURLRequest::kRequestContextXMLHttpRequest
? " The credentials mode of requests initiated by the "
"XMLHttpRequest is controlled by the withCredentials attribute."
: "");
case CORSError::kMissingAllowOriginHeader:
return String::Format(
"%sNo 'Access-Control-Allow-Origin' header is present on the "
"requested resource. Origin '%s' is therefore not allowed access."
"%s%s",
redirect_denied.Utf8().data(), origin.ToString().Utf8().data(),
IsInterestingStatusCode(response_status_code)
? String::Format(" The response had HTTP status code %d.",
response_status_code)
.Utf8()
.data()
: "",
context == WebURLRequest::kRequestContextFetch
? " If an opaque response serves your needs, set the request's "
"mode to 'no-cors' to fetch the resource with CORS disabled."
: "");
case CORSError::kMultipleAllowOriginValues:
return String::Format(
"%sThe 'Access-Control-Allow-Origin' header contains multiple values "
"'%s', but only one is allowed. Origin '%s' is therefore not allowed "
"access.%s",
redirect_denied.Utf8().data(),
response_header.Get(HTTPNames::Access_Control_Allow_Origin)
.Utf8()
.data(),
origin.ToString().Utf8().data(),
context == WebURLRequest::kRequestContextFetch ? kNoCorsInformation
: "");
case CORSError::kInvalidAllowOriginValue:
return String::Format(
"%sThe 'Access-Control-Allow-Origin' header contains the invalid "
"value '%s'. Origin '%s' is therefore not allowed access.%s",
redirect_denied.Utf8().data(),
response_header.Get(HTTPNames::Access_Control_Allow_Origin)
.Utf8()
.data(),
origin.ToString().Utf8().data(),
context == WebURLRequest::kRequestContextFetch ? kNoCorsInformation
: "");
case CORSError::kAllowOriginMismatch:
return String::Format(
"%sThe 'Access-Control-Allow-Origin' header has a value '%s' that is "
"not equal to the supplied origin. Origin '%s' is therefore not "
"allowed access.%s",
redirect_denied.Utf8().data(),
response_header.Get(HTTPNames::Access_Control_Allow_Origin)
.Utf8()
.data(),
origin.ToString().Utf8().data(),
context == WebURLRequest::kRequestContextFetch ? kNoCorsInformation
: "");
case CORSError::kDisallowCredentialsNotSetToTrue:
return String::Format(
"%sThe value of the 'Access-Control-Allow-Credentials' header in "
"the response is '%s' which must be 'true' when the request's "
"credentials mode is 'include'. Origin '%s' is therefore not allowed "
"access.%s",
redirect_denied.Utf8().data(),
response_header.Get(HTTPNames::Access_Control_Allow_Credentials)
.Utf8()
.data(),
origin.ToString().Utf8().data(),
(context == WebURLRequest::kRequestContextXMLHttpRequest
? " The credentials mode of requests initiated by the "
"XMLHttpRequest is controlled by the withCredentials "
"attribute."
: ""));
case CORSError::kPreflightInvalidStatus:
return String::Format(
"Response for preflight has invalid HTTP status code %d.",
response_status_code);
case CORSError::kPreflightMissingAllowExternal:
return WebString(
"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://wicg.github.io/cors-rfc1918/').");
case CORSError::kPreflightInvalidAllowExternal:
return String::Format(
"The 'Access-Control-Allow-External' header in the preflight "
"response for this external request had a value of '%s', not 'true' "
"(This is an experimental header which is defined in "
"'https://wicg.github.io/cors-rfc1918/').",
response_header.Get(HTTPNames::Access_Control_Allow_External)
.Utf8()
.data());
case CORSError::kRedirectDisallowedScheme:
return String::Format(
"%sRedirect location '%s' has a disallowed scheme for cross-origin "
"requests.",
redirect_denied.Utf8().data(),
redirect_url.GetString().Utf8().data());
case CORSError::kRedirectContainsCredentials:
return String::Format(
"%sRedirect location '%s' contains a username and password, which is "
"disallowed for cross-origin requests.",
redirect_denied.Utf8().data(),
redirect_url.GetString().Utf8().data());
}
NOTREACHED();
return WebString();
}
WebHTTPHeaderSet ExtractCorsExposedHeaderNamesList(
network::mojom::FetchCredentialsMode credentials_mode,
const WebURLResponse& response) {
// If a response was fetched via a service worker, it will always have
// CorsExposedHeaderNames set from the Access-Control-Expose-Headers header.
// For requests that didn't come from a service worker, just parse the CORS
// header.
if (response.WasFetchedViaServiceWorker()) {
WebHTTPHeaderSet header_set;
for (const auto& header : response.CorsExposedHeaderNames())
header_set.emplace(header.Ascii().data(), header.Ascii().length());
return header_set;
}
WebHTTPHeaderSet header_set;
HTTPHeaderNameListParser parser(response.HttpHeaderField(
WebString(HTTPNames::Access_Control_Expose_Headers)));
parser.Parse(header_set);
if (credentials_mode != network::mojom::FetchCredentialsMode::kInclude &&
header_set.find("*") != header_set.end()) {
header_set.clear();
for (const auto& header :
response.ToResourceResponse().HttpHeaderFields()) {
CString name = header.key.Ascii();
header_set.emplace(name.data(), name.length());
}
}
return header_set;
}
bool IsOnAccessControlResponseHeaderWhitelist(const WebString& name) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(
WebHTTPHeaderSet, allowed_cross_origin_response_headers,
({
"cache-control", "content-language", "content-type", "expires",
"last-modified", "pragma",
}));
return allowed_cross_origin_response_headers.find(name.Ascii().data()) !=
allowed_cross_origin_response_headers.end();
}
WebString ListOfCORSEnabledURLSchemes() {
return SchemeRegistry::ListOfCORSEnabledURLSchemes();
}
// https://fetch.spec.whatwg.org/#cors-safelisted-method
bool IsCORSSafelistedMethod(const WebString& method) {
return FetchUtils::IsCORSSafelistedMethod(method);
}
bool ContainsOnlyCORSSafelistedOrForbiddenHeaders(const WebHTTPHeaderMap& map) {
return FetchUtils::ContainsOnlyCORSSafelistedOrForbiddenHeaders(
map.GetHTTPHeaderMap());
}
bool IsCORSEnabledRequestMode(network::mojom::FetchRequestMode mode) {
return mode == network::mojom::FetchRequestMode::kCORS ||
mode == network::mojom::FetchRequestMode::kCORSWithForcedPreflight;
}
// No-CORS requests are allowed for all these contexts, and plugin contexts with
// private permission when we set ServiceWorkerMode to None in
// PepperURLLoaderHost.
bool IsNoCORSAllowedContext(
WebURLRequest::RequestContext context,
WebURLRequest::ServiceWorkerMode service_worker_mode) {
switch (context) {
case WebURLRequest::kRequestContextAudio:
case WebURLRequest::kRequestContextFavicon:
case WebURLRequest::kRequestContextFetch:
case WebURLRequest::kRequestContextImage:
case WebURLRequest::kRequestContextObject:
case WebURLRequest::kRequestContextScript:
case WebURLRequest::kRequestContextSharedWorker:
case WebURLRequest::kRequestContextVideo:
case WebURLRequest::kRequestContextWorker:
return true;
case WebURLRequest::kRequestContextPlugin:
return service_worker_mode == WebURLRequest::ServiceWorkerMode::kNone;
default:
return false;
}
}
} // namespace WebCORS
} // namespace blink