blob: dd6b26685b959e830d75bbb63a183e1963606ed4 [file] [log] [blame]
// Copyright 2021 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/local_network_access_checker.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "net/base/transport_info.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/ip_address_space_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "url/gurl.h"
namespace network {
namespace {
using Result = LocalNetworkAccessCheckResult;
using Policy = mojom::LocalNetworkRequestPolicy;
mojom::ClientSecurityStatePtr GetRequestClientSecurityState(
const ResourceRequest& request) {
if (!request.trusted_params.has_value()) {
return nullptr;
}
return request.trusted_params->client_security_state.Clone();
}
// WARNING: This should be kept in sync with similar logic in
// `network::cors::CorsURLLoader::GetClientSecurityState()`.
const mojom::ClientSecurityState* ChooseClientSecurityState(
const mojom::ClientSecurityState* factory_client_security_state,
const mojom::ClientSecurityState* request_client_security_state) {
if (factory_client_security_state) {
// Enforce that only one ClientSecurityState is ever given to us, as this
// is an invariant in the current codebase. In case of a compromised
// renderer process, we might be passed both, in which case we prefer to
// use the factory params' value: contrary to the request params, it is
// always sourced from the browser process.
DCHECK(!request_client_security_state)
<< "Must not provide a ClientSecurityState in both "
"URLLoaderFactoryParams and ResourceRequest::TrustedParams.";
return factory_client_security_state;
}
return request_client_security_state;
}
absl::optional<net::IPAddress> ParseLocalIpFromUrl(const GURL& url) {
net::IPAddress address;
if (!address.AssignFromIPLiteral(url.HostNoBracketsPiece())) {
return absl::nullopt;
}
if (IPAddressToIPAddressSpace(address) != mojom::IPAddressSpace::kLocal) {
return absl::nullopt;
}
return address;
}
} // namespace
LocalNetworkAccessChecker::LocalNetworkAccessChecker(
const ResourceRequest& request,
const mojom::ClientSecurityState* factory_client_security_state,
int32_t url_load_options)
: request_client_security_state_(GetRequestClientSecurityState(request)),
client_security_state_(
ChooseClientSecurityState(factory_client_security_state,
request_client_security_state_.get())),
should_block_local_request_(url_load_options &
mojom::kURLLoadOptionBlockLocalRequest),
target_address_space_(request.target_ip_address_space),
request_initiator_(request.request_initiator) {
SetRequestUrl(request.url);
if (!client_security_state_ ||
client_security_state_->local_network_request_policy ==
mojom::LocalNetworkRequestPolicy::kAllow) {
// No client security state means PNA is implicitly disabled. A policy of
// `kAllow` means PNA is explicitly disabled. In both cases, the target IP
// address space should not be set on the request.
DCHECK_EQ(target_address_space_, mojom::IPAddressSpace::kUnknown)
<< request.url;
}
}
LocalNetworkAccessChecker::~LocalNetworkAccessChecker() = default;
Result LocalNetworkAccessChecker::Check(
const net::TransportInfo& transport_info) {
// If the request URL host was a local IP, record whether we ended up
// connecting to that IP address, unless connecting through a proxy.
// See https://crbug.com/1381471#c2.
if (request_url_local_ip_.has_value() &&
transport_info.type != net::TransportType::kProxied) {
base::UmaHistogramBoolean(
"Security.PrivateNetworkAccess.PrivateIpResolveMatch",
*request_url_local_ip_ == transport_info.endpoint.address());
}
mojom::IPAddressSpace resource_address_space =
TransportInfoToIPAddressSpace(transport_info);
// If we are connecting to a local IP endpoint over HTTP without a target IP
// address space, record whether we could have successfully inferred the
// target IP address space from the request URL.
if (resource_address_space == mojom::IPAddressSpace::kLocal &&
is_request_url_scheme_http_ &&
target_address_space_ == mojom::IPAddressSpace::kUnknown) {
base::UmaHistogramBoolean(
"Security.PrivateNetworkAccess.PrivateIpInferrable",
request_url_local_ip_.has_value());
}
auto result = CheckInternal(resource_address_space);
base::UmaHistogramEnumeration("Security.PrivateNetworkAccess.CheckResult",
result);
if (transport_info.type == net::TransportType::kCached) {
base::UmaHistogramEnumeration(
"Security.PrivateNetworkAccess.CachedResourceCheckResult", result);
}
response_address_space_ = resource_address_space;
return result;
}
void LocalNetworkAccessChecker::ResetForRedirect(const GURL& new_url) {
SetRequestUrl(new_url);
ResetForRetry();
}
void LocalNetworkAccessChecker::ResetForRetry() {
// The target IP address space is no longer relevant, it only applied to the
// URL before the first redirect/retry. Consider the following scenario:
//
// 1. `https://public.example` fetches `http://localhost/foo`
// 2. `OnConnected()` notices that the remote endpoint's IP address space is
// `kLoopback`, fails the request with
// `CorsError::UnexpectedPrivateNetworkAccess`.
// 3. A preflight request is sent with `target_ip_address_space_` set to
// `kLoopback`, succeeds.
// 4. `http://localhost/foo` redirects the GET request to
// `https://public2.example/bar`.
//
// The target IP address space `kLoopback` should not be applied to the new
// connection obtained to `https://public2.example`.
//
// See also: https://crbug.com/1293891
target_address_space_ = mojom::IPAddressSpace::kUnknown;
response_address_space_ = absl::nullopt;
}
mojom::ClientSecurityStatePtr
LocalNetworkAccessChecker::CloneClientSecurityState() const {
if (!client_security_state_) {
return nullptr;
}
return client_security_state_->Clone();
}
mojom::IPAddressSpace LocalNetworkAccessChecker::ClientAddressSpace() const {
if (!client_security_state_) {
return mojom::IPAddressSpace::kUnknown;
}
return client_security_state_->ip_address_space;
}
bool LocalNetworkAccessChecker::IsPolicyPreflightWarn() const {
return client_security_state_ &&
client_security_state_->local_network_request_policy ==
mojom::LocalNetworkRequestPolicy::kPreflightWarn;
}
Result LocalNetworkAccessChecker::CheckInternal(
mojom::IPAddressSpace resource_address_space) {
if (should_block_local_request_ &&
IsLessPublicAddressSpace(resource_address_space,
mojom::IPAddressSpace::kPublic)) {
return Result::kBlockedByLoadOption;
}
if (is_potentially_trustworthy_same_origin_ &&
base::FeatureList::IsEnabled(
features::kLocalNetworkAccessAllowPotentiallyTrustworthySameOrigin)) {
return Result::kAllowedPotentiallyTrustworthySameOrigin;
}
if (!client_security_state_) {
return Result::kAllowedMissingClientSecurityState;
}
mojom::LocalNetworkRequestPolicy policy =
client_security_state_->local_network_request_policy;
if (policy == mojom::LocalNetworkRequestPolicy::kAllow) {
return Result::kAllowedByPolicyAllow;
}
if (target_address_space_ != mojom::IPAddressSpace::kUnknown) {
if (resource_address_space == target_address_space_) {
return Result::kAllowedByTargetIpAddressSpace;
}
if (policy == mojom::LocalNetworkRequestPolicy::kPreflightWarn) {
return Result::kAllowedByPolicyPreflightWarn;
}
return Result::kBlockedByTargetIpAddressSpace;
}
// A single response may connect to two different IP address spaces without
// a redirect in between. This can happen due to split range requests, where
// a single `URLRequest` issues multiple network transactions, or when we
// create a new connection after auth credentials have been provided, etc.
//
// `response_address_space_` behaves similarly to `target_address_space_`,
// except `kUnknown` is also subject to checks (instead
// `response_address_space_ == absl::nullopt` indicates that no check
// should be performed).
if (response_address_space_.has_value() &&
resource_address_space != *response_address_space_) {
// If the policy is `kWarn` or `kPreflightWarn`, the request should not fail
// just because of this check - PNA checks are only experimentally turned on
// for this request. Further checks should not be run, otherwise we might
// return `kBlockedByPolicyPreflightWarn` and trigger a new preflight to be
// sent, thus causing https://crbug.com/1279376 all over again.
if (policy == mojom::LocalNetworkRequestPolicy::kPreflightWarn) {
return Result::kAllowedByPolicyPreflightWarn;
}
// See also https://crbug.com/1334689.
if (policy == mojom::LocalNetworkRequestPolicy::kWarn) {
return Result::kAllowedByPolicyWarn;
}
return Result::kBlockedByInconsistentIpAddressSpace;
}
if (!IsLessPublicAddressSpace(resource_address_space,
client_security_state_->ip_address_space)) {
return Result::kAllowedNoLessPublic;
}
// We use a switch statement to force this code to be amended when values are
// added to the `LocalNetworkRequestPolicy` enum.
switch (policy) {
case Policy::kAllow:
NOTREACHED(); // Should have been handled by the if statement above.
return Result::kAllowedByPolicyAllow;
case Policy::kWarn:
return Result::kAllowedByPolicyWarn;
case Policy::kBlock:
return Result::kBlockedByPolicyBlock;
case Policy::kPreflightWarn:
return Result::kBlockedByPolicyPreflightWarn;
case Policy::kPreflightBlock:
return Result::kBlockedByPolicyPreflightBlock;
}
}
void LocalNetworkAccessChecker::SetRequestUrl(const GURL& url) {
is_request_url_scheme_http_ = url.scheme_piece() == url::kHttpScheme;
request_url_local_ip_ = ParseLocalIpFromUrl(url);
is_potentially_trustworthy_same_origin_ =
IsUrlPotentiallyTrustworthy(url) && request_initiator_.has_value() &&
request_initiator_.value().IsSameOriginWith(url);
}
} // namespace network