| // 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 "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_response_headers.h" |
| #include "services/network/public/cpp/features.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace network { |
| |
| TEST(HeaderUtilTest, IsRequestHeaderSafe) { |
| const struct HeaderKeyValuePair { |
| const char* key; |
| const char* value; |
| bool is_safe; |
| } kHeaders[] = { |
| {"foo", "bar", true}, |
| |
| {net::HttpRequestHeaders::kContentLength, "42", false}, |
| {net::HttpRequestHeaders::kHost, "foo.test", false}, |
| {"Trailer", "header-names", false}, |
| {"Upgrade", "websocket", false}, |
| {"Upgrade", "webbedsocket", false}, |
| {"hOsT", "foo.test", false}, |
| |
| {net::HttpRequestHeaders::kConnection, "Upgrade", false}, |
| {net::HttpRequestHeaders::kConnection, "Close", true}, |
| {net::HttpRequestHeaders::kConnection, "keep-alive", true}, |
| {net::HttpRequestHeaders::kConnection, "keep-alive, close", true}, |
| {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false}, |
| {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false}, |
| {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false}, |
| {net::HttpRequestHeaders::kConnection, "Authorization, keep-alive", |
| false}, |
| {net::HttpRequestHeaders::kConnection, "", true}, |
| {net::HttpRequestHeaders::kConnection, " ", true}, |
| {net::HttpRequestHeaders::kConnection, ",", true}, |
| {net::HttpRequestHeaders::kConnection, ",,,", true}, |
| {net::HttpRequestHeaders::kConnection, " , , ", true}, |
| {net::HttpRequestHeaders::kConnection, "close, ", true}, |
| {net::HttpRequestHeaders::kConnection, ", keep-alive", true}, |
| {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true}, |
| {net::HttpRequestHeaders::kTransferEncoding, "Chunked", false}, |
| {net::HttpRequestHeaders::kTransferEncoding, "Chunky", false}, |
| {"cOnNeCtIoN", "uPgRaDe", false}, |
| {"cOnNeCtIoN", "kEeP-aLiVe", true}, |
| |
| {net::HttpRequestHeaders::kProxyAuthorization, |
| "Basic Zm9vOmJhcg==", false}, |
| {"Proxy-Foo", "bar", false}, |
| {"PrOxY-FoO", "bar", false}, |
| |
| {"X-HTTP-Method-Override", "TRACE", false}, |
| {"x-http-method-override", "trAcE", false}, |
| {"X-HTTP-Method-Override", "GET", true}, |
| {"X-HTTP-Method-Override", "GET, TRACE", false}, |
| {"X-HTTP-Method", "TRACK", false}, |
| {"X-Method-Override", "CONNECT", false}, |
| |
| {"dnt", "1", true}, |
| }; |
| |
| for (const auto& header : kHeaders) { |
| SCOPED_TRACE(header.key); |
| SCOPED_TRACE(header.value); |
| EXPECT_EQ(header.is_safe, IsRequestHeaderSafe(header.key, header.value)); |
| } |
| } |
| |
| TEST(HeaderUtilTest, AreRequestHeadersSafe) { |
| const struct HeaderKeyValuePair { |
| const char* key; |
| const char* value; |
| bool is_safe; |
| } kHeaders[] = { |
| {"foo", "bar", true}, |
| |
| {net::HttpRequestHeaders::kContentLength, "42", false}, |
| {net::HttpRequestHeaders::kHost, "foo.test", false}, |
| {"hOsT", "foo.test", false}, |
| {"Trailer", "header-names", false}, |
| {"Te", "deflate", false}, |
| {"Upgrade", "websocket", false}, |
| {"Upgrade", "webbedsocket", false}, |
| {"Cookie2", "tastiness=5", false}, |
| {"Keep-Alive", "timeout=5, max=1000", false}, |
| {net::HttpRequestHeaders::kTransferEncoding, "gzip", false}, |
| {"Set-Cookie", "foo=bar", false}, |
| |
| {net::HttpRequestHeaders::kConnection, "Upgrade", false}, |
| {net::HttpRequestHeaders::kConnection, "Close", true}, |
| {net::HttpRequestHeaders::kConnection, "keep-alive", true}, |
| {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false}, |
| {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false}, |
| {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false}, |
| {net::HttpRequestHeaders::kConnection, "", true}, |
| {net::HttpRequestHeaders::kConnection, " ", true}, |
| {net::HttpRequestHeaders::kConnection, ",", true}, |
| {net::HttpRequestHeaders::kConnection, ",,,", true}, |
| {net::HttpRequestHeaders::kConnection, " , , ", true}, |
| {net::HttpRequestHeaders::kConnection, "close, ", true}, |
| {net::HttpRequestHeaders::kConnection, ", keep-alive", true}, |
| {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true}, |
| {net::HttpRequestHeaders::kTransferEncoding, "Chunked", false}, |
| {net::HttpRequestHeaders::kTransferEncoding, "Chunky", false}, |
| {"cOnNeCtIoN", "uPgRaDe", false}, |
| |
| {net::HttpRequestHeaders::kProxyAuthorization, |
| "Basic Zm9vOmJhcg==", false}, |
| {"Proxy-Foo", "bar", false}, |
| {"PrOxY-FoO", "bar", false}, |
| |
| {"X-HTTP-Method-Override", "TRACE", false}, |
| {"x-http-method-override", "trAcE", false}, |
| {"X-HTTP-Method-Override", "GET", true}, |
| {"X-HTTP-Method-Override", "GET, TRACE", false}, |
| {"X-HTTP-Method", "TRACK", false}, |
| {"X-Method-Override", "CONNECT", false}, |
| |
| {"dnt", "1", true}, |
| }; |
| |
| // Check each header in isolation, and all combinations of two header |
| // key/value pairs that have different keys. |
| for (const auto& header1 : kHeaders) { |
| net::HttpRequestHeaders request_headers1; |
| request_headers1.SetHeader(header1.key, header1.value); |
| EXPECT_EQ(header1.is_safe, AreRequestHeadersSafe(request_headers1)); |
| |
| for (const auto& header2 : kHeaders) { |
| if (base::EqualsCaseInsensitiveASCII(header1.key, header2.key)) |
| continue; |
| SCOPED_TRACE(header1.key); |
| SCOPED_TRACE(header1.value); |
| SCOPED_TRACE(header2.key); |
| SCOPED_TRACE(header2.value); |
| |
| net::HttpRequestHeaders request_headers2; |
| request_headers2.SetHeader(header1.key, header1.value); |
| request_headers2.SetHeader(header2.key, header2.value); |
| EXPECT_EQ(header1.is_safe && header2.is_safe, |
| AreRequestHeadersSafe(request_headers2)); |
| } |
| } |
| } |
| |
| TEST(HeaderUtilTest, ParseReferrerPolicy) { |
| struct TestCase { |
| const char* referrer_policy_value; |
| mojom::ReferrerPolicy expected_referrer_policy; |
| }; |
| const TestCase kTests[] = { |
| {"", mojom::ReferrerPolicy::kDefault}, |
| {"no-referrer", mojom::ReferrerPolicy::kNever}, |
| {"no-referrer-when-downgrade", |
| mojom::ReferrerPolicy::kNoReferrerWhenDowngrade}, |
| {"origin", mojom::ReferrerPolicy::kOrigin}, |
| {"origin-when-cross-origin", |
| mojom::ReferrerPolicy::kOriginWhenCrossOrigin}, |
| {"unsafe-url", mojom::ReferrerPolicy::kAlways}, |
| {"same-origin", mojom::ReferrerPolicy::kSameOrigin}, |
| {"strict-origin", mojom::ReferrerPolicy::kStrictOrigin}, |
| {"strict-origin-when-cross-origin", |
| mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin}, |
| // Unknown value. |
| {"unknown-value", mojom::ReferrerPolicy::kDefault}, |
| // Multiple values. |
| {"no-referrer,unsafe-url", mojom::ReferrerPolicy::kAlways}, |
| }; |
| |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(::testing::Message() << "referrer_policy_value: \"" |
| << test.referrer_policy_value << "\""); |
| |
| auto headers = net::HttpResponseHeaders::TryToCreate(base::StringPrintf( |
| "HTTP/1.1 200 OK\r\nReferrer-Policy: %s", test.referrer_policy_value)); |
| ASSERT_TRUE(headers); |
| |
| mojom::ReferrerPolicy parsed_policy = ParseReferrerPolicy(*headers); |
| EXPECT_EQ(parsed_policy, test.expected_referrer_policy); |
| } |
| } |
| |
| TEST(HeaderUtilTest, ContainsForbiddenSecurityHeader) { |
| net::HttpRequestHeaders headers; |
| |
| // Normal case |
| headers.SetHeader("Sec-CH-UA", "Normal Value"); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| std::string value; |
| auto value_opt = headers.GetHeader("Sec-CH-UA"); |
| ASSERT_TRUE(value_opt.has_value()); |
| value = *value_opt; |
| EXPECT_EQ(value, "Normal Value"); |
| |
| // Truncation case |
| std::string long_value(2000, 'a'); |
| headers.SetHeader("Sec-CH-UA-Long", long_value); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| value_opt = headers.GetHeader("Sec-CH-UA-Long"); |
| ASSERT_TRUE(value_opt.has_value()); |
| value = *value_opt; |
| EXPECT_EQ(value.length(), 1024u); |
| EXPECT_EQ(value, long_value.substr(0, 1024)); |
| |
| // Boundary cases for Sec-CH- |
| std::string value_1023(1023, 'a'); |
| headers.SetHeader("Sec-CH-UA-1023", value_1023); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-CH-UA-1023"), value_1023); |
| |
| std::string value_1024(1024, 'a'); |
| headers.SetHeader("Sec-CH-UA-1024", value_1024); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-CH-UA-1024"), value_1024); |
| |
| std::string value_1025(1025, 'a'); |
| headers.SetHeader("Sec-CH-UA-1025", value_1025); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(headers.GetHeader("Sec-CH-UA-1025")->length(), 1024u); |
| |
| // Non-Sec-CH- header should not be truncated even if long |
| headers.SetHeader("X-Custom-Header", long_value); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| value_opt = headers.GetHeader("X-Custom-Header"); |
| ASSERT_TRUE(value_opt.has_value()); |
| value = *value_opt; |
| EXPECT_EQ(value.length(), 2000u); |
| |
| // Sec-Shared-Storage-Data-Origin boundary cases |
| net::HttpRequestHeaders origin_headers; |
| std::string origin_267(267, 'a'); |
| origin_headers.SetHeader("Sec-Shared-Storage-Data-Origin", origin_267); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(origin_headers)); |
| |
| std::string origin_268(268, 'a'); |
| origin_headers.SetHeader("Sec-Shared-Storage-Data-Origin", origin_268); |
| EXPECT_TRUE(ContainsForbiddenSecurityHeader(origin_headers)); |
| |
| // Forbidden Sec- header |
| headers.SetHeader("Sec-Invalid", "value"); |
| std::string forbidden_header_name; |
| EXPECT_TRUE(ContainsForbiddenSecurityHeader(headers, &forbidden_header_name)); |
| EXPECT_EQ(forbidden_header_name, "Sec-Invalid"); |
| |
| // Sec-Fetch- headers should be forbidden by default |
| net::HttpRequestHeaders fetch_headers; |
| fetch_headers.SetHeader("Sec-Fetch-Site", "same-origin"); |
| EXPECT_TRUE(ContainsForbiddenSecurityHeader(fetch_headers)); |
| |
| // Sec-GPC cases |
| net::HttpRequestHeaders gpc_headers; |
| gpc_headers.SetHeader("Sec-GPC", "1"); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(gpc_headers)); |
| |
| gpc_headers.SetHeader("Sec-GPC", "0"); |
| EXPECT_TRUE(ContainsForbiddenSecurityHeader(gpc_headers)); |
| |
| gpc_headers.SetHeader("Sec-GPC", "2"); |
| EXPECT_TRUE(ContainsForbiddenSecurityHeader(gpc_headers)); |
| } |
| |
| TEST(HeaderUtilTest, |
| ContainsForbiddenSecurityHeader_SecSpeculationTags_Truncation) { |
| net::HttpRequestHeaders headers; |
| |
| // Normal case (< 2048) |
| std::string normal_value = "tag1,tag2"; |
| headers.SetHeader("Sec-Speculation-Tags", normal_value); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| auto value_opt = headers.GetHeader("Sec-Speculation-Tags"); |
| ASSERT_TRUE(value_opt.has_value()); |
| EXPECT_EQ(*value_opt, normal_value); |
| |
| // Truncation case with comma |
| std::string long_value = std::string(2000, 'a') + "," + std::string(100, 'b'); |
| // total size = 2000 + 1 + 100 = 2101 > 2048 |
| headers.SetHeader("Sec-Speculation-Tags", long_value); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| value_opt = headers.GetHeader("Sec-Speculation-Tags"); |
| ASSERT_TRUE(value_opt.has_value()); |
| std::string value = *value_opt; |
| EXPECT_LE(value.length(), 2048u); |
| // It should be truncated at the last comma before 2048. |
| // 2000 'a's + 1 comma = 2001 bytes. The next is 'b'. |
| // So it should truncate at the comma. |
| EXPECT_EQ(value, std::string(2000, 'a')); |
| |
| // Truncation case without comma (fallback to 2048) |
| std::string very_long_tag(2500, 'c'); |
| headers.SetHeader("Sec-Speculation-Tags", very_long_tag); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| value_opt = headers.GetHeader("Sec-Speculation-Tags"); |
| ASSERT_TRUE(value_opt.has_value()); |
| value = *value_opt; |
| EXPECT_EQ(value.length(), 2048u); |
| EXPECT_EQ(value, very_long_tag.substr(0, 2048)); |
| |
| // Boundary cases for Sec-Speculation-Tags |
| std::string value_2047(2047, 'a'); |
| headers.SetHeader("Sec-Speculation-Tags", value_2047); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), value_2047); |
| |
| std::string value_2048(2048, 'a'); |
| headers.SetHeader("Sec-Speculation-Tags", value_2048); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), value_2048); |
| |
| std::string value_2049(2049, 'a'); |
| headers.SetHeader("Sec-Speculation-Tags", value_2049); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(headers.GetHeader("Sec-Speculation-Tags")->length(), 2048u); |
| |
| // Boundary cases with comma |
| std::string comma_at_2047 = std::string(2047, 'a') + ",b"; |
| headers.SetHeader("Sec-Speculation-Tags", comma_at_2047); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), std::string(2047, 'a')); |
| |
| std::string comma_at_2048 = std::string(2048, 'a') + ",b"; |
| headers.SetHeader("Sec-Speculation-Tags", comma_at_2048); |
| EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers)); |
| EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), std::string(2048, 'a')); |
| } |
| |
| } // namespace network |