blob: aec61af078b9c5b7b5ccbf5f2bfc3b9fbf305c85 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/cors/preflight_controller.h"
#include <algorithm>
#include <optional>
#include <vector>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/unguessable_token.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/base/network_isolation_key.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/log/net_log.h"
#include "net/log/net_log_source.h"
#include "net/log/net_log_with_source.h"
#include "services/network/cors/cors_util.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/cors/cors_error_status.h"
#include "services/network/public/cpp/devtools_observer_util.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "services/network/public/mojom/devtools_observer.mojom.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/network/public/mojom/parsed_headers.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_loader_network_service_observer.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace network::cors {
namespace {
int RetrieveCacheFlags(int load_flags) {
return load_flags & (net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
net::LOAD_DISABLE_CACHE);
}
std::optional<std::string> GetHeaderString(
const scoped_refptr<net::HttpResponseHeaders>& headers,
const std::string& header_name) {
if (!headers) {
return std::nullopt;
}
return headers->GetNormalizedHeader(header_name);
}
// Algorithm step 3 of the CORS-preflight fetch,
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0, that requires
// - CORS-safelisted request-headers excluded
// - duplicates excluded
// - sorted lexicographically
// - byte-lowercased
std::string CreateAccessControlRequestHeadersHeader(
const net::HttpRequestHeaders& headers,
bool is_revalidating) {
// Exclude the forbidden headers because they may be added by the user
// agent. They must be checked separately and rejected for
// JavaScript-initiated requests.
std::vector<std::string> filtered_headers =
CorsUnsafeNotForbiddenRequestHeaderNames(headers.GetHeaderVector(),
is_revalidating);
if (filtered_headers.empty())
return std::string();
// Sort header names lexicographically.
std::sort(filtered_headers.begin(), filtered_headers.end());
return base::JoinString(filtered_headers, ",");
}
std::unique_ptr<ResourceRequest> CreatePreflightRequest(
const ResourceRequest& request,
bool tainted,
const net::NetLogWithSource& net_log_for_actual_request,
const std::optional<base::UnguessableToken>& devtools_request_id) {
DCHECK(!request.url.has_username());
DCHECK(!request.url.has_password());
std::unique_ptr<ResourceRequest> preflight_request =
std::make_unique<ResourceRequest>();
// Algorithm step 1 through 5 of the CORS-preflight fetch,
// https://fetch.spec.whatwg.org/#cors-preflight-fetch.
preflight_request->url = request.url;
preflight_request->method = net::HttpRequestHeaders::kOptionsMethod;
preflight_request->priority = request.priority;
preflight_request->destination = request.destination;
preflight_request->referrer = request.referrer;
preflight_request->referrer_policy = request.referrer_policy;
preflight_request->mode = mojom::RequestMode::kCors;
preflight_request->credentials_mode = mojom::CredentialsMode::kOmit;
preflight_request->load_flags = RetrieveCacheFlags(request.load_flags);
preflight_request->resource_type = request.resource_type;
preflight_request->fetch_window_id = request.fetch_window_id;
preflight_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
kDefaultAcceptHeaderValue);
preflight_request->headers.SetHeader(
header_names::kAccessControlRequestMethod, request.method);
std::string request_headers = CreateAccessControlRequestHeadersHeader(
request.headers, request.is_revalidating);
if (!request_headers.empty()) {
preflight_request->headers.SetHeader(
header_names::kAccessControlRequestHeaders, request_headers);
}
preflight_request->target_ip_address_space = request.target_ip_address_space;
if (request.trusted_params.has_value()) {
preflight_request->trusted_params = ResourceRequest::TrustedParams();
// Copy the client security state as well, if set in the request's trusted
// params. Note that we clone the pointer unconditionally if the original
// request has trusted params, but that the cloned pointer may be null. It
// is unclear whether it is safe to copy all the trusted params, so we only
// copy what we need for PNA.
//
// This is useful when the client security state is not specified through
// the URL loader factory params, typically when a single URL loader factory
// is shared by a few different client contexts. This is the case for
// navigations and interest group auctions.
preflight_request->trusted_params->client_security_state =
request.trusted_params->client_security_state.Clone();
// The preflight should use an IsolationInfo corresponding to that of the
// request (if available) but with `IsolationInfo::RequestType::kOther`
// since the preflights themselves are not considered navigations, and with
// an empty `net::SiteForCookies()` since cookies aren't sent with these
// requests.
if (!request.trusted_params->isolation_info.IsEmpty()) {
preflight_request->trusted_params->isolation_info =
net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther,
*request.trusted_params->isolation_info.top_frame_origin(),
*request.trusted_params->isolation_info.frame_origin(),
net::SiteForCookies(),
request.trusted_params->isolation_info.nonce());
// Ensure consistency of this IsolationInfo's SiteForCookies with the
// SiteForCookies in the request.
CHECK(preflight_request->site_for_cookies.IsEquivalent(
preflight_request->trusted_params->isolation_info
.site_for_cookies()));
}
}
DCHECK(request.request_initiator);
preflight_request->request_initiator = request.request_initiator;
preflight_request->headers.SetHeader(
net::HttpRequestHeaders::kOrigin,
(tainted ? url::Origin() : *request.request_initiator).Serialize());
// We normally set User-Agent down in the network stack, but the DevTools
// emulation override is applied on a higher level (renderer or browser),
// so copy User-Agent from the original request, if present.
// TODO(caseq, morlovich): do the same for client hints.
std::optional<std::string> user_agent =
request.headers.GetHeader(net::HttpRequestHeaders::kUserAgent);
if (user_agent) {
preflight_request->headers.SetHeader(net::HttpRequestHeaders::kUserAgent,
*user_agent);
}
// Additional headers that the algorithm in the spec does not require, but
// it's better that CORS preflight requests have them.
preflight_request->headers.SetHeader("Sec-Fetch-Mode", "cors");
if (devtools_request_id) {
// Set `enable_load_timing` flag to make URLLoader fill the LoadTimingInfo
// in URLResponseHead, which will be sent to DevTools.
preflight_request->enable_load_timing = true;
// Set `devtools_request_id` to make URLLoader send the raw request and
// the raw response to DevTools.
preflight_request->devtools_request_id = devtools_request_id->ToString();
}
preflight_request->is_fetch_like_api = request.is_fetch_like_api;
preflight_request->is_fetch_later_api = request.is_fetch_later_api;
preflight_request->is_favicon = request.is_favicon;
// Set `net_log_reference_info` to reference actual request from preflight
// request in NetLog.
preflight_request->net_log_reference_info =
net_log_for_actual_request.source();
net::NetLogSource net_log_source_for_preflight = net::NetLogSource(
net::NetLogSourceType::URL_REQUEST, net::NetLog::Get()->NextID());
net_log_for_actual_request.AddEventReferencingSource(
net::NetLogEventType::CORS_PREFLIGHT_URL_REQUEST,
net_log_source_for_preflight);
// Set `net_log_create_info` to specify NetLog source used in preflight
// URL Request.
preflight_request->net_log_create_info = net_log_source_for_preflight;
return preflight_request;
}
// Performs a CORS access check on the CORS-preflight response parameters.
// According to the note at https://fetch.spec.whatwg.org/#cors-preflight-fetch
// step 6, even for a preflight check, `credentials_mode` should be checked on
// the actual request rather than preflight one.
base::expected<void, CorsErrorStatus> CheckPreflightAccess(
const GURL& response_url,
const int response_status_code,
const std::optional<std::string>& allow_origin_header,
const std::optional<std::string>& allow_credentials_header,
mojom::CredentialsMode actual_credentials_mode,
const url::Origin& origin) {
// Step 7 of https://fetch.spec.whatwg.org/#cors-preflight-fetch
auto cors_result =
CheckAccess(response_url, allow_origin_header, allow_credentials_header,
actual_credentials_mode, origin);
const bool has_ok_status = IsSuccessfulStatus(response_status_code);
if (cors_result.has_value()) {
if (has_ok_status) {
return base::ok();
}
return base::unexpected(
CorsErrorStatus(mojom::CorsError::kPreflightInvalidStatus));
}
// Prefer using a preflight specific error code.
const auto map_to_preflight_error_codes = [](mojom::CorsError error) {
switch (error) {
case mojom::CorsError::kWildcardOriginNotAllowed:
return mojom::CorsError::kPreflightWildcardOriginNotAllowed;
case mojom::CorsError::kMissingAllowOriginHeader:
return mojom::CorsError::kPreflightMissingAllowOriginHeader;
case mojom::CorsError::kMultipleAllowOriginValues:
return mojom::CorsError::kPreflightMultipleAllowOriginValues;
case mojom::CorsError::kInvalidAllowOriginValue:
return mojom::CorsError::kPreflightInvalidAllowOriginValue;
case mojom::CorsError::kAllowOriginMismatch:
return mojom::CorsError::kPreflightAllowOriginMismatch;
case mojom::CorsError::kInvalidAllowCredentials:
return mojom::CorsError::kPreflightInvalidAllowCredentials;
default:
NOTREACHED();
}
};
cors_result.error().cors_error =
map_to_preflight_error_codes(cors_result.error().cors_error);
return cors_result;
}
std::unique_ptr<PreflightResult> CreatePreflightResult(
const GURL& final_url,
const mojom::URLResponseHead& head,
const ResourceRequest& original_request,
bool tainted,
const mojom::ClientSecurityStatePtr& client_security_state,
base::WeakPtr<mojo::Remote<mojom::DevToolsObserver>> devtools_observer,
std::optional<CorsErrorStatus>* detected_error_status) {
CHECK(detected_error_status);
auto check_result = CheckPreflightAccess(
final_url, head.headers ? head.headers->response_code() : 0,
GetHeaderString(head.headers, header_names::kAccessControlAllowOrigin),
GetHeaderString(head.headers,
header_names::kAccessControlAllowCredentials),
original_request.credentials_mode,
tainted ? url::Origin() : *original_request.request_initiator);
if (!check_result.has_value()) {
*detected_error_status = std::move(check_result.error());
return nullptr;
}
std::optional<mojom::CorsError> error;
auto result = PreflightResult::Create(
original_request.credentials_mode,
GetHeaderString(head.headers, header_names::kAccessControlAllowMethods),
GetHeaderString(head.headers, header_names::kAccessControlAllowHeaders),
GetHeaderString(head.headers, header_names::kAccessControlMaxAge),
&error);
if (error)
*detected_error_status = CorsErrorStatus(*error);
return result;
}
std::optional<CorsErrorStatus> CheckPreflightResult(
const PreflightResult& result,
const ResourceRequest& original_request,
NonWildcardRequestHeadersSupport non_wildcard_request_headers_support,
bool acam_preflight_spec_conformant) {
std::optional<CorsErrorStatus> status = result.EnsureAllowedCrossOriginMethod(
original_request.method, acam_preflight_spec_conformant);
if (status)
return status;
return result.EnsureAllowedCrossOriginHeaders(
original_request.headers, original_request.is_revalidating,
non_wildcard_request_headers_support);
}
} // namespace
class PreflightController::PreflightLoader final {
public:
PreflightLoader(
PreflightController* controller,
CompletionCallback completion_callback,
int32_t request_id,
const ResourceRequest& request,
WithTrustedHeaderClient with_trusted_header_client,
NonWildcardRequestHeadersSupport non_wildcard_request_headers_support,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag,
const net::NetworkIsolationKey& network_isolation_key,
mojom::ClientSecurityStatePtr client_security_state,
base::WeakPtr<mojo::Remote<mojom::DevToolsObserver>> devtools_observer,
const net::NetLogWithSource net_log,
bool acam_preflight_spec_conformant,
mojo::PendingRemote<mojom::URLLoaderNetworkServiceObserver>
url_loader_network_service_observer)
: controller_(controller),
completion_callback_(std::move(completion_callback)),
original_request_(request),
non_wildcard_request_headers_support_(
non_wildcard_request_headers_support),
tainted_(tainted),
network_isolation_key_(network_isolation_key),
client_security_state_(std::move(client_security_state)),
devtools_observer_(std::move(devtools_observer)),
net_log_(net_log),
acam_preflight_spec_conformant_(acam_preflight_spec_conformant),
url_loader_network_service_observer_(
std::move(url_loader_network_service_observer)) {
if (devtools_observer_)
devtools_request_id_ = base::UnguessableToken::Create();
auto preflight_request =
CreatePreflightRequest(request, tainted, net_log, devtools_request_id_);
if (devtools_observer_ && *devtools_observer_) {
DCHECK(devtools_request_id_);
network::mojom::URLRequestDevToolsInfoPtr request_info =
network::ExtractDevToolsInfo(*preflight_request);
(*devtools_observer_)
->OnCorsPreflightRequest(
*devtools_request_id_, preflight_request->headers,
std::move(request_info), original_request_.url,
original_request_.devtools_request_id.value_or(""));
}
loader_ =
SimpleURLLoader::Create(std::move(preflight_request), annotation_tag);
loader_->SetRequestID(request_id);
uint32_t options = mojom::kURLLoadOptionAsCorsPreflight;
if (with_trusted_header_client) {
options |= mojom::kURLLoadOptionUseHeaderClient;
}
loader_->SetURLLoaderFactoryOptions(options);
}
PreflightLoader(const PreflightLoader&) = delete;
PreflightLoader& operator=(const PreflightLoader&) = delete;
void Request(mojom::URLLoaderFactory* loader_factory) {
DCHECK(loader_);
loader_->SetOnRedirectCallback(base::BindRepeating(
&PreflightLoader::HandleRedirect, base::Unretained(this)));
loader_->SetOnResponseStartedCallback(base::BindOnce(
&PreflightLoader::HandleResponseHeader, base::Unretained(this)));
loader_->DownloadToString(
loader_factory,
base::BindOnce(&PreflightLoader::HandleResponseBody,
base::Unretained(this)),
0);
}
private:
void HandleRedirect(const GURL& url_before_redirect,
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* to_be_removed_headers) {
if (devtools_observer_ && *devtools_observer_) {
DCHECK(devtools_request_id_);
(*devtools_observer_)
->OnCorsPreflightRequestCompleted(
*devtools_request_id_,
network::URLLoaderCompletionStatus(net::ERR_INVALID_REDIRECT));
}
std::move(completion_callback_)
.Run(net::ERR_FAILED,
CorsErrorStatus(mojom::CorsError::kPreflightDisallowedRedirect),
false);
RemoveFromController();
// `this` is deleted here.
}
void HandleResponseHeader(const GURL& final_url,
const mojom::URLResponseHead& head) {
if (devtools_observer_ && *devtools_observer_) {
DCHECK(devtools_request_id_);
mojom::URLResponseHeadDevToolsInfoPtr head_info =
ExtractDevToolsInfo(head);
(*devtools_observer_)
->OnCorsPreflightResponse(*devtools_request_id_,
original_request_.url,
std::move(head_info));
(*devtools_observer_)
->OnCorsPreflightRequestCompleted(
*devtools_request_id_,
network::URLLoaderCompletionStatus(net::OK));
}
std::optional<CorsErrorStatus> detected_error_status;
std::unique_ptr<PreflightResult> result = CreatePreflightResult(
final_url, head, original_request_, tainted_, client_security_state_,
devtools_observer_, &detected_error_status);
if (!result) {
std::move(completion_callback_)
.Run(net::ERR_FAILED, std::move(detected_error_status), false);
return;
}
// NOTE: `detected_error_status` may be non-nullopt if a PNA warning was
// encountered in `CreatePreflightResult()`.
// Only log if there is a result to log.
net_log_.AddEvent(net::NetLogEventType::CORS_PREFLIGHT_RESULT,
[&result] { return result->NetLogParams(); });
// Preflight succeeded. Check `original_request_` with `result`.
net::Error net_error = net::OK;
std::optional<CorsErrorStatus> check_error_status = CheckPreflightResult(
*result, original_request_, non_wildcard_request_headers_support_,
acam_preflight_spec_conformant_);
// Avoid overwriting if `CheckPreflightResult()` succeeds, just in case
// there was a PNA warning in `detected_error_status`.
// TODO(crbug.com/40204695): Simplify this by always overwriting
// `detected_error_status` once preflights are always enforced.
if (check_error_status.has_value()) {
net_error = net::ERR_FAILED;
detected_error_status = std::move(check_error_status);
}
FinishHandleResponseHeader(net_error, std::move(detected_error_status),
std::move(result));
}
void FinishHandleResponseHeader(
net::Error net_error,
std::optional<CorsErrorStatus> detected_error_status,
std::unique_ptr<PreflightResult> result) {
bool has_authorization_covered_by_wildcard =
result->HasAuthorizationCoveredByWildcard(original_request_.headers);
if (!(original_request_.load_flags & net::LOAD_DISABLE_CACHE) &&
net_error == net::OK) {
controller_->AppendToCache(*original_request_.request_initiator,
original_request_.url, network_isolation_key_,
original_request_.target_ip_address_space,
std::move(result));
}
std::move(completion_callback_)
.Run(net_error, detected_error_status,
has_authorization_covered_by_wildcard);
}
void HandleResponseBody(std::unique_ptr<std::string> response_body) {
const int error = loader_->NetError();
const std::optional<URLLoaderCompletionStatus>& status =
loader_->CompletionStatus();
if (!completion_callback_.is_null()) {
// As HandleResponseHeader() isn't called due to a request failure, such
// as unknown hosts. unreachable remote, reset by peer, and so on, we
// still hold `completion_callback_` to invoke.
if (devtools_observer_ && *devtools_observer_) {
DCHECK(devtools_request_id_);
(*devtools_observer_)
->OnCorsPreflightRequestCompleted(
*devtools_request_id_,
status.has_value() ? *status
: network::URLLoaderCompletionStatus(error));
}
std::move(completion_callback_)
.Run(error,
status.has_value() ? status->cors_error_status : std::nullopt,
false);
}
RemoveFromController();
// `this` is deleted here.
}
// Removes `this` instance from `controller_`. Once the method returns, `this`
// is already removed.
void RemoveFromController() { controller_->RemoveLoader(this); }
// PreflightController owns all PreflightLoader instances, and should outlive.
const raw_ptr<PreflightController> controller_;
// Holds SimpleURLLoader instance for the CORS-preflight request.
std::unique_ptr<SimpleURLLoader> loader_;
// Holds caller's information.
PreflightController::CompletionCallback completion_callback_;
const ResourceRequest original_request_;
const NonWildcardRequestHeadersSupport non_wildcard_request_headers_support_;
const bool tainted_;
std::optional<base::UnguessableToken> devtools_request_id_;
const net::NetworkIsolationKey network_isolation_key_;
const mojom::ClientSecurityStatePtr client_security_state_;
base::WeakPtr<mojo::Remote<mojom::DevToolsObserver>> devtools_observer_;
const net::NetLogWithSource net_log_;
const bool acam_preflight_spec_conformant_;
mojo::Remote<mojom::URLLoaderNetworkServiceObserver>
url_loader_network_service_observer_;
};
// static
std::unique_ptr<ResourceRequest>
PreflightController::CreatePreflightRequestForTesting(
const ResourceRequest& request,
bool tainted) {
return CreatePreflightRequest(
request, tainted,
net::NetLogWithSource::Make(net::NetLog::Get(),
net::NetLogSourceType::URL_REQUEST),
/*devtools_request_id=*/std::nullopt);
}
// static
std::unique_ptr<PreflightResult>
PreflightController::CreatePreflightResultForTesting(
const GURL& final_url,
const mojom::URLResponseHead& head,
const ResourceRequest& original_request,
bool tainted,
std::optional<CorsErrorStatus>* detected_error_status) {
return CreatePreflightResult(
final_url, head, original_request, tainted,
/*client_security_state=*/nullptr,
/*devtools_observer=*/
base::WeakPtr<mojo::Remote<mojom::DevToolsObserver>>(),
detected_error_status);
}
// static
base::expected<void, CorsErrorStatus>
PreflightController::CheckPreflightAccessForTesting(
const GURL& response_url,
const int response_status_code,
const std::optional<std::string>& allow_origin_header,
const std::optional<std::string>& allow_credentials_header,
mojom::CredentialsMode actual_credentials_mode,
const url::Origin& origin) {
return CheckPreflightAccess(response_url, response_status_code,
allow_origin_header, allow_credentials_header,
actual_credentials_mode, origin);
}
PreflightController::PreflightController(NetworkService* network_service)
: network_service_(network_service) {}
PreflightController::~PreflightController() = default;
void PreflightController::PerformPreflightCheck(
CompletionCallback callback,
int32_t request_id,
const ResourceRequest& request,
WithTrustedHeaderClient with_trusted_header_client,
NonWildcardRequestHeadersSupport non_wildcard_request_headers_support,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag,
mojom::URLLoaderFactory* loader_factory,
const net::IsolationInfo& isolation_info,
mojom::ClientSecurityStatePtr client_security_state,
base::WeakPtr<mojo::Remote<mojom::DevToolsObserver>> devtools_observer,
const net::NetLogWithSource& net_log,
bool acam_preflight_spec_conformant,
mojo::PendingRemote<mojom::URLLoaderNetworkServiceObserver>
url_loader_network_service_observer) {
DCHECK(request.request_initiator);
const net::NetworkIsolationKey& network_isolation_key =
!isolation_info.IsEmpty()
? isolation_info.network_isolation_key()
: request.trusted_params.has_value()
? request.trusted_params->isolation_info.network_isolation_key()
: net::NetworkIsolationKey();
if (!RetrieveCacheFlags(request.load_flags) &&
cache_.CheckIfRequestCanSkipPreflight(
request.request_initiator.value(), request.url, network_isolation_key,
request.target_ip_address_space, request.credentials_mode,
request.method, request.headers, request.is_revalidating, net_log,
acam_preflight_spec_conformant)) {
std::move(callback).Run(net::OK, std::nullopt, false);
return;
}
auto emplaced_pair = loaders_.emplace(std::make_unique<PreflightLoader>(
this, std::move(callback), request_id, request,
with_trusted_header_client, non_wildcard_request_headers_support, tainted,
annotation_tag, network_isolation_key, std::move(client_security_state),
devtools_observer, net_log, acam_preflight_spec_conformant,
std::move(url_loader_network_service_observer)));
(*emplaced_pair.first)->Request(loader_factory);
}
void PreflightController::ClearCorsPreflightCache(
mojom::ClearDataFilterPtr url_filter) {
cache_.ClearCache(std::move(url_filter));
}
void PreflightController::RemoveLoader(PreflightLoader* loader) {
auto it = loaders_.find(loader);
CHECK(it != loaders_.end());
loaders_.erase(it);
}
void PreflightController::AppendToCache(
const url::Origin& origin,
const GURL& url,
const net::NetworkIsolationKey& network_isolation_key,
mojom::IPAddressSpace target_ip_address_space,
std::unique_ptr<PreflightResult> result) {
cache_.AppendEntry(origin, url, network_isolation_key,
target_ip_address_space, std::move(result));
}
} // namespace network::cors