blob: 64fb2172fac0d41d5f7313540c0d61b5f8f70c31 [file] [log] [blame]
// Copyright 2019 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/sec_header_helpers.h"
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/types/optional_ref.h"
#include "net/base/isolation_info.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/cookies/cookie_util.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/initiator_lock_compatibility.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/request_destination.h"
#include "services/network/public/cpp/request_mode.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "url/origin.h"
namespace network {
namespace {
constexpr std::string_view kSecFetchMode = "Sec-Fetch-Mode";
constexpr std::string_view kSecFetchSite = "Sec-Fetch-Site";
constexpr std::string_view kSecFetchUser = "Sec-Fetch-User";
constexpr std::string_view kSecFetchDest = "Sec-Fetch-Dest";
constexpr std::string_view kSecFetchStorageAccess = "Sec-Fetch-Storage-Access";
constexpr std::string_view kSecFetchFrameTop = "Sec-Fetch-Frame-Top";
constexpr std::string_view kSecFetchAncestors = "Sec-Fetch-Frame-Ancestors";
constexpr char kSecFetchStorageAccessOutcomeHistogram[] =
"API.StorageAccessHeader.SecFetchStorageAccessOutcome";
std::string_view OriginRelationString(
std::optional<net::OriginRelation> relation) {
if (!relation.has_value()) {
return "none";
}
switch (relation.value()) {
case net::OriginRelation::kSameOrigin:
return "same-origin";
case net::OriginRelation::kSameSite:
return "same-site";
case net::OriginRelation::kCrossSite:
return "cross-site";
}
}
// Walk through a URL chain and a pending redirect url to calculate their
// relationship to the origin.
net::OriginRelation GetRelationOfURLChainToOrigin(
const std::vector<GURL>& request_chain,
const url::Origin& origin,
base::optional_ref<const GURL> pending_redirect_url) {
CHECK(!request_chain.empty());
auto origin_relation = net::OriginRelation::kSameOrigin;
for (const GURL& target_url : request_chain) {
origin_relation =
std::max(origin_relation, net::GetOriginRelation(target_url, origin));
}
if (pending_redirect_url.has_value()) {
origin_relation =
std::max(origin_relation,
net::GetOriginRelation(pending_redirect_url.value(), origin));
}
return origin_relation;
}
// Returns the relationship between a request (including its url and url_chain)
// and the request's top_frame_origin in the form of a
// net::OriginRelation.
std::optional<net::OriginRelation> GetFrameTopRelation(
const net::URLRequest& request,
base::optional_ref<const GURL> pending_redirect_url) {
if (request.isolation_info().IsEmpty() ||
request.isolation_info().request_type() ==
net::IsolationInfo::RequestType::kMainFrame) {
return std::nullopt;
}
const url::Origin& top_frame_origin =
request.isolation_info().top_frame_origin().value();
// We only care about the current URL for the top frame relation, which for
// redirects is `pending_redirect_url`.
return net::GetOriginRelation(
pending_redirect_url ? pending_redirect_url.value() : request.url(),
top_frame_origin);
}
std::optional<net::OriginRelation> GetInitiatorRelation(
const net::URLRequest& request,
base::optional_ref<const GURL> pending_redirect_url,
const mojom::URLLoaderFactoryParams& factory_params,
const cors::OriginAccessList& origin_access_list) {
// Browser-initiated requests with no initiator origin will send
// `Sec-Fetch-Site: None`.
if (!request.initiator().has_value()) {
// CorsURLLoaderFactory::IsValidRequest verifies that only the browser
// process may initiate requests with no request initiator.
DCHECK_EQ(factory_params.process_id, mojom::kBrowserProcessId);
return std::nullopt;
}
const url::Origin& initiator = request.initiator().value();
// Privileged requests initiated from a "non-webby" context will send
// `Sec-Fetch-Site: None` while unprivileged ones will send
// `Sec-Fetch-Site: cross-site`.
if (factory_params.unsafe_non_webby_initiator) {
cors::OriginAccessList::AccessState access_state =
origin_access_list.CheckAccessState(initiator, request.url());
bool is_privileged =
(access_state == cors::OriginAccessList::AccessState::kAllowed);
if (is_privileged) {
return std::nullopt;
}
return net::OriginRelation::kCrossSite;
}
// Other requests default to `kSameOrigin`, and walk through the request's URL
// chain to calculate the correct value.
return GetRelationOfURLChainToOrigin(request.url_chain(), initiator,
pending_redirect_url);
}
char const* GetSecFetchStorageAccessHeaderValue(
net::cookie_util::StorageAccessStatus storage_access_status) {
switch (storage_access_status) {
case net::cookie_util::StorageAccessStatus::kInactive:
return "inactive";
case net::cookie_util::StorageAccessStatus::kActive:
return "active";
case net::cookie_util::StorageAccessStatus::kNone:
return "none";
}
NOTREACHED();
}
net::cookie_util::SecFetchStorageAccessOutcome
ComputeSecFetchStorageAccessOutcome(const net::URLRequest& request,
mojom::CredentialsMode credentials_mode) {
if (request.storage_access_status().IsSet() &&
!request.storage_access_status().GetStatusForThirdPartyContext()) {
return net::cookie_util::SecFetchStorageAccessOutcome::
kOmittedStatusMissing;
}
if (credentials_mode != mojom::CredentialsMode::kInclude) {
return net::cookie_util::SecFetchStorageAccessOutcome::
kOmittedRequestOmitsCredentials;
}
CHECK(request.storage_access_status().IsSet());
switch (
request.storage_access_status().GetStatusForThirdPartyContext().value()) {
case net::cookie_util::StorageAccessStatus::kInactive:
return net::cookie_util::SecFetchStorageAccessOutcome::kValueInactive;
case net::cookie_util::StorageAccessStatus::kActive:
return net::cookie_util::SecFetchStorageAccessOutcome::kValueActive;
case net::cookie_util::StorageAccessStatus::kNone:
return net::cookie_util::SecFetchStorageAccessOutcome::kValueNone;
}
NOTREACHED();
}
// Sec-Fetch-Site
void SetSecFetchSiteHeader(net::URLRequest& request,
base::optional_ref<const GURL> pending_redirect_url,
const mojom::URLLoaderFactoryParams& factory_params,
const cors::OriginAccessList& origin_access_list) {
std::optional<net::OriginRelation> relation = GetInitiatorRelation(
request, pending_redirect_url, factory_params, origin_access_list);
request.SetExtraRequestHeaderByName(kSecFetchSite,
OriginRelationString(relation),
/* overwrite = */ true);
}
// Sec-Fetch-Mode
void SetSecFetchModeHeader(net::URLRequest& request,
network::mojom::RequestMode mode) {
std::string header_value = RequestModeToString(mode);
request.SetExtraRequestHeaderByName(kSecFetchMode, header_value, false);
}
// Sec-Fetch-User
void SetSecFetchUserHeader(net::URLRequest& request, bool has_user_activation) {
if (has_user_activation)
request.SetExtraRequestHeaderByName(kSecFetchUser, "?1", true);
else
request.RemoveRequestHeaderByName(kSecFetchUser);
}
// Sec-Fetch-Dest
void SetSecFetchDestHeader(net::URLRequest& request,
network::mojom::RequestDestination dest) {
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest
// If r's destination is the empty string, set header's value to the string
// "empty". Otherwise, set header's value to r's destination.
std::string header_value = RequestDestinationToString(
dest, EmptyRequestDestinationOption::kUseFiveCharEmptyString);
request.SetExtraRequestHeaderByName(kSecFetchDest, header_value, true);
}
// Sec-Fetch-Storage-Access
void SetSecFetchStorageAccessHeader(net::URLRequest& request,
mojom::CredentialsMode credentials_mode) {
base::UmaHistogramEnumeration(
kSecFetchStorageAccessOutcomeHistogram,
ComputeSecFetchStorageAccessOutcome(request, credentials_mode));
if (credentials_mode != mojom::CredentialsMode::kInclude ||
(request.storage_access_status().IsSet() &&
!request.storage_access_status().GetStatusForThirdPartyContext())) {
// A credentials mode of "same-origin" or "omit" prevents including cookies
// on the request in the first place, so we don't bother to include the
// `Sec-Fetch-Storage-Access` header in that case.
//
// To ensure that an erroneous value isn't sent by mistake (and that
// consumers aren't allowed to override the correct "omitted" value), we
// clear any existing value.
request.RemoveRequestHeaderByName(kSecFetchStorageAccess);
return;
}
CHECK(request.storage_access_status().IsSet());
request.SetExtraRequestHeaderByName(
kSecFetchStorageAccess,
GetSecFetchStorageAccessHeaderValue(request.storage_access_status()
.GetStatusForThirdPartyContext()
.value()),
/*overwrite=*/true);
}
// Sec-Fetch-Frame-Top
void SetSecFetchFrameTop(net::URLRequest& request,
base::optional_ref<const GURL> pending_redirect_url) {
if (!base::FeatureList::IsEnabled(features::kFrameTopHeader)) {
return;
}
std::optional<net::OriginRelation> relation =
GetFrameTopRelation(request, pending_redirect_url);
if (!relation.has_value()) {
return;
}
request.SetExtraRequestHeaderByName(kSecFetchFrameTop,
OriginRelationString(relation),
/*overwrite=*/true);
}
// Sec-Fetch-Frame-Ancestors
void SetSecFetchFrameAncestorsHeader(
net::URLRequest& request,
base::optional_ref<const GURL> pending_redirect_url) {
if (!base::FeatureList::IsEnabled(features::kFrameAncestorsHeader) ||
request.isolation_info().IsEmpty()) {
return;
}
std::optional<net::IsolationInfo::FrameAncestorRelation>
header_relation_value;
// We need to manually calculate the value for kMainFrame requests. This is
// because redirect requests will not yet have the updated IsolationInfo.
// Since kMainFrame requests are inherently same-origin, we can safely do
// this.
if (request.isolation_info().request_type() ==
net::IsolationInfo::RequestType::kMainFrame) {
header_relation_value =
net::IsolationInfo::FrameAncestorRelation::kSameOrigin;
} else {
header_relation_value = net::IsolationInfo::ComputeNewFrameAncestorRelation(
request.isolation_info().frame_ancestor_relation(),
url::Origin::Create(pending_redirect_url ? pending_redirect_url.value()
: request.url()),
request.isolation_info().top_frame_origin().value());
}
if (header_relation_value) {
request.SetExtraRequestHeaderByName(
kSecFetchAncestors,
net::IsolationInfo::FrameAncestorRelationString(
header_relation_value.value()),
/*overwrite=*/true);
}
}
} // namespace
void SetFetchMetadataHeaders(
net::URLRequest& request,
network::mojom::RequestMode mode,
bool has_user_activation,
network::mojom::RequestDestination dest,
base::optional_ref<const GURL> pending_redirect_url,
const mojom::URLLoaderFactoryParams& factory_params,
const cors::OriginAccessList& origin_access_list,
mojom::CredentialsMode credentials_mode) {
DCHECK_NE(0u, request.url_chain().size());
// Only append the header to potentially trustworthy URLs.
const GURL& target_url = pending_redirect_url.has_value()
? pending_redirect_url.value()
: request.url();
if (!IsUrlPotentiallyTrustworthy(target_url))
return;
SetSecFetchSiteHeader(request, pending_redirect_url, factory_params,
origin_access_list);
SetSecFetchModeHeader(request, mode);
SetSecFetchUserHeader(request, has_user_activation);
SetSecFetchDestHeader(request, dest);
SetSecFetchStorageAccessHeader(request, credentials_mode);
SetSecFetchFrameTop(request, pending_redirect_url);
SetSecFetchFrameAncestorsHeader(request, pending_redirect_url);
}
void MaybeRemoveSecHeaders(net::URLRequest& request,
const GURL& pending_redirect_url) {
// If our redirect destination is not trusted it would not have had sec-ch-
// or sec-fetch- prefixed headers added to it. Our previous hops may have
// added these headers if the current url is trustworthy though so we should
// try to remove these now.
if (IsUrlPotentiallyTrustworthy(request.url()) &&
!IsUrlPotentiallyTrustworthy(pending_redirect_url)) {
// Check each of our request headers and if it is a "sec-ch-" or
// "sec-fetch-" prefixed header we'll remove it.
const net::HttpRequestHeaders::HeaderVector request_headers =
request.extra_request_headers().GetHeaderVector();
for (const auto& header : request_headers) {
if (StartsWith(header.key, "sec-ch-",
base::CompareCase::INSENSITIVE_ASCII) ||
StartsWith(header.key, "sec-fetch-",
base::CompareCase::INSENSITIVE_ASCII)) {
request.RemoveRequestHeaderByName(header.key);
}
}
}
}
} // namespace network