| // 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/public/cpp/header_util.h" |
| |
| #include <map> |
| #include <string> |
| #include <vector> |
| |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "net/base/mime_sniffer.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/http/http_util.h" |
| #include "services/network/public/cpp/cors/cors.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| // Headers that consumers are not trusted to set. All "Proxy-" prefixed messages |
| // are blocked inline. The"Authorization" auth header is deliberately not |
| // included, since OAuth requires websites be able to set it directly. These are |
| // a subset of headers forbidden by the fetch spec. |
| // |
| // This list has some values in common with |
| // https://fetch.spec.whatwg.org/#forbidden-header-name, but excludes some |
| // values that are still set by the caller in Chrome. |
| const char* kUnsafeHeaders[] = { |
| // This is determined by the upload body and set by net/. A consumer |
| // overriding that could allow for Bad Things. |
| net::HttpRequestHeaders::kContentLength, |
| |
| // Disallow setting the Host header because it can conflict with specified |
| // URL and logic related to isolating sites. |
| net::HttpRequestHeaders::kHost, |
| |
| // Trailers are not supported. |
| "Trailer", |
| "Te", |
| |
| // Websockets use a different API. |
| "Upgrade", |
| |
| // Obsolete header, and network stack manages headers itself. |
| "Cookie2", |
| |
| // Not supported by net/. |
| "Keep-Alive", |
| |
| // Forbidden by the fetch spec. |
| net::HttpRequestHeaders::kTransferEncoding, |
| |
| // Semantically a response header, so not useful on requests. |
| "Set-Cookie", |
| |
| // TODO(mmenke): Figure out what to do about the remaining headers: |
| // Cookie, Date, Expect, Referer, Via. |
| }; |
| |
| } // namespace |
| |
| bool IsRequestHeaderSafe(std::string_view key, std::string_view value) { |
| for (const auto* header : kUnsafeHeaders) { |
| if (base::EqualsCaseInsensitiveASCII(header, key)) |
| return false; |
| } |
| |
| // The Connection header is a comma-separated list of tokens. Per RFC 9110 |
| // section 7.6.1, intermediaries treat each listed token as the name of a |
| // header to remove before forwarding, so only allow the connection-management |
| // options that the network stack itself uses. Websockets use a different API, |
| // so "upgrade" is not needed here. |
| if (base::EqualsCaseInsensitiveASCII(key, |
| net::HttpRequestHeaders::kConnection)) { |
| net::HttpUtil::ValuesIterator tokens(value, ','); |
| while (tokens.GetNext()) { |
| if (!base::EqualsCaseInsensitiveASCII(tokens.value(), "close") && |
| !base::EqualsCaseInsensitiveASCII(tokens.value(), "keep-alive")) { |
| return false; |
| } |
| } |
| } |
| |
| // Proxy headers are destined for the proxy, so shouldn't be set by callers. |
| if (base::StartsWith(key, "Proxy-", base::CompareCase::INSENSITIVE_ASCII)) |
| return false; |
| |
| if (base::EqualsCaseInsensitiveASCII(key, "X-HTTP-Method") || |
| base::EqualsCaseInsensitiveASCII(key, "X-HTTP-Method-Override") || |
| base::EqualsCaseInsensitiveASCII(key, "X-Method-Override")) { |
| net::HttpUtil::ValuesIterator method_iterator(value, ','); |
| while (method_iterator.GetNext()) { |
| if (cors::IsForbiddenMethod(method_iterator.value())) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| bool AreRequestHeadersSafe(const net::HttpRequestHeaders& request_headers) { |
| net::HttpRequestHeaders::Iterator it(request_headers); |
| |
| while (it.GetNext()) { |
| if (!IsRequestHeaderSafe(it.name(), it.value())) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ContainsForbiddenSecurityHeader(net::HttpRequestHeaders& headers, |
| std::string* out_forbidden_header_name) { |
| static const bool enabled = |
| base::FeatureList::IsEnabled(features::kRestrictForbiddenSecurityHeaders); |
| if (!enabled) { |
| return false; |
| } |
| |
| std::map<std::string, std::string> headers_to_truncate; |
| |
| auto sanitize_and_check_security_header = [&](std::string_view name, |
| std::string_view value) { |
| // Client Hints are harmless and set by the renderer. |
| if (base::StartsWith(name, "Sec-CH-", |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| size_t size = value.size(); |
| base::UmaHistogramCounts10000("NetworkService.SecCHHeaderSize", size); |
| if (size > 1024) { |
| headers_to_truncate[std::string(name)] = |
| std::string(value.substr(0, 1024)); |
| } |
| return true; |
| } |
| // Sec-Purpose is used for prefetch hints and contains short strings. |
| if (base::EqualsCaseInsensitiveASCII(name, "Sec-Purpose")) { |
| return value.size() < 256; |
| } |
| // Sec-GPC is allowed with value "1". |
| // https://w3c.github.io/gpc/#the-sec-gpc-header-field-for-http-requests |
| if (base::EqualsCaseInsensitiveASCII(name, "Sec-GPC")) { |
| return value == "1"; |
| } |
| // Browsing Topics API headers contain structured interest tokens. |
| if (base::EqualsCaseInsensitiveASCII(name, "Sec-Browsing-Topics")) { |
| return value.size() < 1024; |
| } |
| // Shared Storage fetch headers use structured boolean "?1". |
| if (base::EqualsCaseInsensitiveASCII(name, "Sec-Shared-Storage-Writable")) { |
| return value == "?1"; |
| } |
| // Shared Storage data origin headers contain origin URLs. |
| if (base::EqualsCaseInsensitiveASCII(name, |
| "Sec-Shared-Storage-Data-Origin")) { |
| return value.size() <= 267; |
| } |
| // Speculation Rules headers contain comma-separated tokens. |
| if (base::EqualsCaseInsensitiveASCII(name, "Sec-Speculation-Tags")) { |
| size_t size = value.size(); |
| base::UmaHistogramCounts10000( |
| "NetworkService.SecSpeculationTagsHeaderSize", size); |
| if (size > 2048) { |
| std::string_view truncated_value = value.substr(0, 2048); |
| size_t last_comma = truncated_value.rfind(','); |
| if (last_comma != std::string_view::npos) { |
| truncated_value = truncated_value.substr(0, last_comma); |
| } |
| headers_to_truncate[std::string(name)] = std::string(truncated_value); |
| } |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| net::HttpRequestHeaders::Iterator it(headers); |
| while (it.GetNext()) { |
| if (base::StartsWith(it.name(), "Sec-", |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| if (!sanitize_and_check_security_header(it.name(), it.value())) { |
| if (out_forbidden_header_name) { |
| *out_forbidden_header_name = std::string(it.name()); |
| } |
| return true; |
| } |
| } |
| } |
| |
| for (const auto& [name, value] : headers_to_truncate) { |
| headers.SetHeader(name, value); |
| } |
| |
| return false; |
| } |
| |
| mojom::ReferrerPolicy ParseReferrerPolicy( |
| const net::HttpResponseHeaders& response_headers) { |
| using enum net::ReferrerPolicy; |
| using enum mojom::ReferrerPolicy; |
| std::optional<std::string> referrer_policy_header = |
| response_headers.GetNormalizedHeader("Referrer-Policy"); |
| if (!referrer_policy_header) { |
| return kDefault; |
| } |
| |
| std::optional<net::ReferrerPolicy> net_policy = |
| net::ReferrerPolicyFromHeader(*referrer_policy_header); |
| |
| if (!net_policy) { |
| return kDefault; |
| } |
| |
| switch (net_policy.value()) { |
| case NO_REFERRER: |
| return kNever; |
| case CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE: |
| return kNoReferrerWhenDowngrade; |
| case ORIGIN: |
| return kOrigin; |
| case ORIGIN_ONLY_ON_TRANSITION_CROSS_ORIGIN: |
| return kOriginWhenCrossOrigin; |
| case CLEAR_ON_TRANSITION_CROSS_ORIGIN: |
| return kSameOrigin; |
| case ORIGIN_CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE: |
| return kStrictOrigin; |
| case REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN: |
| return kStrictOriginWhenCrossOrigin; |
| case NEVER_CLEAR: |
| return kAlways; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| bool ShouldSniffContent(const GURL& url, |
| const mojom::URLResponseHead& response) { |
| return net::ShouldSniffMimeType(url, response.headers.get(), |
| response.mime_type); |
| } |
| |
| bool IsSuccessfulStatus(int status) { |
| // This contains successful 2xx status code. |
| return status >= net::HTTP_OK && status < net::HTTP_MULTIPLE_CHOICES; |
| } |
| |
| } // namespace network |