blob: d4155c460610990f5a813b79e04a7e8b1dd5721d [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 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 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 "CrossOriginAccessControl.h"
#include "CachedResourceRequest.h"
#include "CrossOriginEmbedderPolicy.h"
#include "CrossOriginPreflightResultCache.h"
#include "HTTPHeaderNames.h"
#include "HTTPParsers.h"
#include "LegacySchemeRegistry.h"
#include "Page.h"
#include "ResourceRequest.h"
#include "ResourceResponse.h"
#include "RuntimeApplicationChecks.h"
#include "SecurityOrigin.h"
#include "SecurityPolicy.h"
#include <mutex>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/AtomString.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
bool isOnAccessControlSimpleRequestMethodAllowlist(const String& method)
{
return method == "GET" || method == "HEAD" || method == "POST";
}
bool isSimpleCrossOriginAccessRequest(const String& method, const HTTPHeaderMap& headerMap)
{
if (!isOnAccessControlSimpleRequestMethodAllowlist(method))
return false;
for (const auto& header : headerMap) {
if (!header.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(header.keyAsHTTPHeaderName.value(), header.value))
return false;
}
return true;
}
void updateRequestReferrer(ResourceRequest& request, ReferrerPolicy referrerPolicy, const String& outgoingReferrer)
{
String newOutgoingReferrer = SecurityPolicy::generateReferrerHeader(referrerPolicy, request.url(), outgoingReferrer);
if (newOutgoingReferrer.isEmpty())
request.clearHTTPReferrer();
else
request.setHTTPReferrer(newOutgoingReferrer);
}
void updateRequestForAccessControl(ResourceRequest& request, SecurityOrigin& securityOrigin, StoredCredentialsPolicy storedCredentialsPolicy)
{
request.removeCredentials();
request.setAllowCookies(storedCredentialsPolicy == StoredCredentialsPolicy::Use);
request.setHTTPOrigin(securityOrigin.toString());
}
ResourceRequest createAccessControlPreflightRequest(const ResourceRequest& request, SecurityOrigin& securityOrigin, const String& referrer)
{
ResourceRequest preflightRequest(request.url());
static const double platformDefaultTimeout = 0;
preflightRequest.setTimeoutInterval(platformDefaultTimeout);
updateRequestForAccessControl(preflightRequest, securityOrigin, StoredCredentialsPolicy::DoNotUse);
preflightRequest.setHTTPMethod("OPTIONS");
preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestMethod, request.httpMethod());
preflightRequest.setPriority(request.priority());
preflightRequest.setFirstPartyForCookies(request.firstPartyForCookies());
preflightRequest.setIsAppInitiated(request.isAppInitiated());
if (!referrer.isNull())
preflightRequest.setHTTPReferrer(referrer);
const HTTPHeaderMap& requestHeaderFields = request.httpHeaderFields();
if (!requestHeaderFields.isEmpty()) {
Vector<String> unsafeHeaders;
for (auto& headerField : requestHeaderFields) {
if (!headerField.keyAsHTTPHeaderName || !isCrossOriginSafeRequestHeader(*headerField.keyAsHTTPHeaderName, headerField.value))
unsafeHeaders.append(headerField.key.convertToASCIILowercase());
}
std::sort(unsafeHeaders.begin(), unsafeHeaders.end(), WTF::codePointCompareLessThan);
StringBuilder headerBuffer;
bool appendComma = false;
for (const auto& headerField : unsafeHeaders) {
if (appendComma)
headerBuffer.append(',');
else
appendComma = true;
headerBuffer.append(headerField);
}
if (!headerBuffer.isEmpty())
preflightRequest.setHTTPHeaderField(HTTPHeaderName::AccessControlRequestHeaders, headerBuffer.toString());
}
return preflightRequest;
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#create-a-potential-cors-request
CachedResourceRequest createPotentialAccessControlRequest(ResourceRequest&& request, ResourceLoaderOptions&& options, Document& document, const String& crossOriginAttribute, SameOriginFlag sameOriginFlag)
{
ASSERT(options.mode == FetchOptions::Mode::NoCors);
if (!crossOriginAttribute.isNull())
options.mode = FetchOptions::Mode::Cors;
else if (sameOriginFlag == SameOriginFlag::Yes)
options.mode = FetchOptions::Mode::SameOrigin;
if (options.mode != FetchOptions::Mode::NoCors) {
if (auto* page = document.page()) {
if (page->shouldDisableCorsForRequestTo(request.url()))
options.mode = FetchOptions::Mode::NoCors;
}
}
if (crossOriginAttribute.isNull()) {
CachedResourceRequest cachedRequest { WTFMove(request), WTFMove(options) };
cachedRequest.setOrigin(document.securityOrigin());
return cachedRequest;
}
FetchOptions::Credentials credentials = equalLettersIgnoringASCIICase(crossOriginAttribute, "omit")
? FetchOptions::Credentials::Omit : equalLettersIgnoringASCIICase(crossOriginAttribute, "use-credentials")
? FetchOptions::Credentials::Include : FetchOptions::Credentials::SameOrigin;
options.credentials = credentials;
switch (credentials) {
case FetchOptions::Credentials::Include:
options.storedCredentialsPolicy = StoredCredentialsPolicy::Use;
break;
case FetchOptions::Credentials::SameOrigin:
options.storedCredentialsPolicy = document.securityOrigin().canRequest(request.url()) ? StoredCredentialsPolicy::Use : StoredCredentialsPolicy::DoNotUse;
break;
case FetchOptions::Credentials::Omit:
options.storedCredentialsPolicy = StoredCredentialsPolicy::DoNotUse;
}
CachedResourceRequest cachedRequest { WTFMove(request), WTFMove(options) };
updateRequestForAccessControl(cachedRequest.resourceRequest(), document.securityOrigin(), options.storedCredentialsPolicy);
return cachedRequest;
}
String validateCrossOriginRedirectionURL(const URL& redirectURL)
{
if (!LegacySchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(redirectURL.protocol().toStringWithoutCopying()))
return makeString("not allowed to follow a cross-origin CORS redirection with non CORS scheme");
if (redirectURL.hasCredentials())
return makeString("redirection URL ", redirectURL.string(), " has credentials");
return { };
}
OptionSet<HTTPHeadersToKeepFromCleaning> httpHeadersToKeepFromCleaning(const HTTPHeaderMap& headers)
{
OptionSet<HTTPHeadersToKeepFromCleaning> headersToKeep;
if (headers.contains(HTTPHeaderName::ContentType))
headersToKeep.add(HTTPHeadersToKeepFromCleaning::ContentType);
if (headers.contains(HTTPHeaderName::Referer))
headersToKeep.add(HTTPHeadersToKeepFromCleaning::Referer);
if (headers.contains(HTTPHeaderName::Origin))
headersToKeep.add(HTTPHeadersToKeepFromCleaning::Origin);
if (headers.contains(HTTPHeaderName::UserAgent))
headersToKeep.add(HTTPHeadersToKeepFromCleaning::UserAgent);
if (headers.contains(HTTPHeaderName::AcceptEncoding))
headersToKeep.add(HTTPHeadersToKeepFromCleaning::AcceptEncoding);
return headersToKeep;
}
void cleanHTTPRequestHeadersForAccessControl(ResourceRequest& request, OptionSet<HTTPHeadersToKeepFromCleaning> headersToKeep)
{
// Remove headers that may have been added by the network layer that cause access control to fail.
if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::ContentType)) {
auto contentType = request.httpContentType();
if (!contentType.isNull() && !isCrossOriginSafeRequestHeader(HTTPHeaderName::ContentType, contentType))
request.clearHTTPContentType();
}
if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::Referer))
request.clearHTTPReferrer();
if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::Origin))
request.clearHTTPOrigin();
if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::UserAgent))
request.clearHTTPUserAgent();
if (!headersToKeep.contains(HTTPHeadersToKeepFromCleaning::AcceptEncoding))
request.clearHTTPAcceptEncoding();
}
CrossOriginAccessControlCheckDisabler& CrossOriginAccessControlCheckDisabler::singleton()
{
ASSERT(!isInNetworkProcess());
static NeverDestroyed<CrossOriginAccessControlCheckDisabler> disabler;
return disabler.get();
}
void CrossOriginAccessControlCheckDisabler::setCrossOriginAccessControlCheckEnabled(bool enabled)
{
ASSERT(!isInNetworkProcess());
m_accessControlCheckEnabled = enabled;
}
bool CrossOriginAccessControlCheckDisabler::crossOriginAccessControlCheckEnabled() const
{
ASSERT(!isInNetworkProcess());
return m_accessControlCheckEnabled;
}
Expected<void, String> passesAccessControlCheck(const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, const SecurityOrigin& securityOrigin, const CrossOriginAccessControlCheckDisabler* checkDisabler)
{
// 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.
const String& accessControlOriginString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowOrigin);
bool starAllowed = storedCredentialsPolicy == StoredCredentialsPolicy::DoNotUse;
if (!starAllowed)
starAllowed = checkDisabler && !checkDisabler->crossOriginAccessControlCheckEnabled();
if (accessControlOriginString == "*" && starAllowed)
return { };
String securityOriginString = securityOrigin.toString();
if (accessControlOriginString != securityOriginString) {
if (accessControlOriginString == "*")
return makeUnexpected("Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true."_s);
if (accessControlOriginString.find(',') != notFound)
return makeUnexpected("Access-Control-Allow-Origin cannot contain more than one origin."_s);
return makeUnexpected(makeString("Origin ", securityOriginString, " is not allowed by Access-Control-Allow-Origin.", " Status code: ", response.httpStatusCode()));
}
if (storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
const String& accessControlCredentialsString = response.httpHeaderField(HTTPHeaderName::AccessControlAllowCredentials);
if (accessControlCredentialsString != "true")
return makeUnexpected("Credentials flag is true, but Access-Control-Allow-Credentials is not \"true\"."_s);
}
return { };
}
Expected<void, String> validatePreflightResponse(PAL::SessionID sessionID, const ResourceRequest& request, const ResourceResponse& response, StoredCredentialsPolicy storedCredentialsPolicy, const SecurityOrigin& securityOrigin, const CrossOriginAccessControlCheckDisabler* checkDisabler)
{
if (!response.isSuccessful())
return makeUnexpected(makeString("Preflight response is not successful. Status code: ", response.httpStatusCode()));
auto accessControlCheckResult = passesAccessControlCheck(response, storedCredentialsPolicy, securityOrigin, checkDisabler);
if (!accessControlCheckResult)
return accessControlCheckResult;
auto result = CrossOriginPreflightResultCacheItem::create(storedCredentialsPolicy, response);
if (!result.has_value())
return makeUnexpected(WTFMove(result.error()));
auto entry = WTFMove(result.value());
auto errorDescription = entry->validateMethodAndHeaders(request.httpMethod(), request.httpHeaderFields());
CrossOriginPreflightResultCache::singleton().appendEntry(sessionID, securityOrigin.toString(), request.url(), entry.moveToUniquePtr());
if (errorDescription)
return makeUnexpected(WTFMove(*errorDescription));
return { };
}
// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check
static inline bool shouldCrossOriginResourcePolicyCancelLoad(CrossOriginEmbedderPolicyValue coep, const SecurityOrigin& origin, const ResourceResponse& response, ForNavigation forNavigation)
{
if (forNavigation == ForNavigation::Yes && coep != CrossOriginEmbedderPolicyValue::RequireCORP)
return false;
if (response.isNull() || origin.canRequest(response.url()))
return false;
auto policy = parseCrossOriginResourcePolicyHeader(response.httpHeaderField(HTTPHeaderName::CrossOriginResourcePolicy));
// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check (step 4).
if ((policy == CrossOriginResourcePolicy::None || policy == CrossOriginResourcePolicy::Invalid) && coep == CrossOriginEmbedderPolicyValue::RequireCORP)
return true;
if (policy == CrossOriginResourcePolicy::SameOrigin)
return true;
if (policy == CrossOriginResourcePolicy::SameSite) {
if (origin.isUnique())
return true;
#if ENABLE(PUBLIC_SUFFIX_LIST)
if (!RegistrableDomain::uncheckedCreateFromHost(origin.host()).matches(response.url()))
return true;
#endif
if (origin.protocol() == "http" && response.url().protocol() == "https")
return true;
}
return false;
}
std::optional<ResourceError> validateCrossOriginResourcePolicy(CrossOriginEmbedderPolicyValue coep, const SecurityOrigin& origin, const URL& requestURL, const ResourceResponse& response, ForNavigation forNavigation)
{
if (shouldCrossOriginResourcePolicyCancelLoad(coep, origin, response, forNavigation))
return ResourceError { errorDomainWebKitInternal, 0, requestURL, makeString("Cancelled load to ", response.url().stringCenterEllipsizedToLength(), " because it violates the resource's Cross-Origin-Resource-Policy response header."), ResourceError::Type::AccessControl };
return std::nullopt;
}
std::optional<ResourceError> validateRangeRequestedFlag(const ResourceRequest& request, const ResourceResponse& response)
{
if (response.isRangeRequested() && response.httpStatusCode() == 206 && response.type() == ResourceResponse::Type::Opaque && !request.hasHTTPHeaderField(HTTPHeaderName::Range))
return ResourceError({ }, 0, response.url(), { }, ResourceError::Type::General);
return std::nullopt;
}
} // namespace WebCore