| // 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 |