diff --git a/browser/net/brave_network_delegate_base.cc b/browser/net/brave_network_delegate_base.cc index 2323372c7572..0f80eb8f6347 100644 --- a/browser/net/brave_network_delegate_base.cc +++ b/browser/net/brave_network_delegate_base.cc @@ -1,10 +1,12 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public +/* Copyright (c) 2019 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "brave/browser/net/brave_network_delegate_base.h" #include +#include #include "base/task/post_task.h" #include "brave/common/pref_names.h" @@ -19,9 +21,11 @@ #include "content/public/browser/browser_thread.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/web_contents.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "net/url_request/url_request.h" using content::BrowserThread; +using net::HttpResponseHeaders; using net::URLRequest; namespace { @@ -40,6 +44,41 @@ content::WebContents* GetWebContentsFromProcessAndFrameId(int render_process_id, } // namespace +base::flat_set* TrackableSecurityHeaders() { + static base::NoDestructor> + kTrackableSecurityHeaders(base::flat_set{ + "Strict-Transport-Security", "Expect-CT", "Public-Key-Pins", + "Public-Key-Pins-Report-Only"}); + return kTrackableSecurityHeaders.get(); +} + +void RemoveTrackableSecurityHeadersForThirdParty( + URLRequest* request, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers) { + if (!request || !request->top_frame_origin().has_value() || + (!original_response_headers && !override_response_headers->get())) { + return; + } + + auto top_frame_origin = request->top_frame_origin().value(); + auto request_url = request->url(); + + if (net::registry_controlled_domains::SameDomainOrHost( + request_url, top_frame_origin, + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { + return; + } + + if (!override_response_headers->get()) { + *override_response_headers = + new net::HttpResponseHeaders(original_response_headers->raw_headers()); + } + for (auto header : *TrackableSecurityHeaders()) { + (*override_response_headers)->RemoveHeader(header.as_string()); + } +} + BraveNetworkDelegateBase::BraveNetworkDelegateBase( extensions::EventRouterForwarder* event_router) : ChromeNetworkDelegate(event_router), referral_headers_list_(nullptr) { @@ -68,12 +107,11 @@ void BraveNetworkDelegateBase::InitPrefChangeRegistrarOnUI() { void BraveNetworkDelegateBase::OnReferralHeadersChanged() { DCHECK_CURRENTLY_ON(BrowserThread::UI); if (const base::ListValue* referral_headers = - g_browser_process->local_state()->GetList(kReferralHeaders)) { + g_browser_process->local_state()->GetList(kReferralHeaders)) { base::PostTaskWithTraits( FROM_HERE, {BrowserThread::IO}, base::Bind(&BraveNetworkDelegateBase::SetReferralHeaders, - base::Unretained(this), - referral_headers->DeepCopy())); + base::Unretained(this), referral_headers->DeepCopy())); } } @@ -124,6 +162,9 @@ int BraveNetworkDelegateBase::OnHeadersReceived( const net::HttpResponseHeaders* original_response_headers, scoped_refptr* override_response_headers, GURL* allowed_unsafe_redirect_url) { + RemoveTrackableSecurityHeadersForThirdParty( + request, original_response_headers, override_response_headers); + if (headers_received_callbacks_.empty() || !request) { return ChromeNetworkDelegate::OnHeadersReceived( request, std::move(callback), original_response_headers, diff --git a/browser/net/brave_network_delegate_base.h b/browser/net/brave_network_delegate_base.h index bd4900741df0..c8af89c0b97c 100644 --- a/browser/net/brave_network_delegate_base.h +++ b/browser/net/brave_network_delegate_base.h @@ -1,10 +1,18 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public +/* Copyright (c) 2019 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef BRAVE_BROWSER_NET_BRAVE_NETWORK_DELEGATE_BASE_H_ #define BRAVE_BROWSER_NET_BRAVE_NETWORK_DELEGATE_BASE_H_ +#include +#include +#include +#include + +#include "base/containers/flat_set.h" +#include "base/strings/string_piece.h" #include "brave/browser/net/url_context.h" #include "chrome/browser/net/chrome_network_delegate.h" #include "content/public/browser/browser_thread.h" @@ -20,6 +28,13 @@ namespace net { class URLRequest; } +base::flat_set* TrackableSecurityHeaders(); + +void RemoveTrackableSecurityHeadersForThirdParty( + net::URLRequest* request, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers); + // BraveNetworkDelegateBase is the central point from within the Brave code to // add hooks into the network stack. class BraveNetworkDelegateBase : public ChromeNetworkDelegate { @@ -28,7 +43,8 @@ class BraveNetworkDelegateBase : public ChromeNetworkDelegate { using ResponseListener = base::Callback; - BraveNetworkDelegateBase(extensions::EventRouterForwarder* event_router); + explicit BraveNetworkDelegateBase( + extensions::EventRouterForwarder* event_router); ~BraveNetworkDelegateBase() override; bool IsRequestIdentifierValid(uint64_t request_identifier); diff --git a/browser/net/brave_network_delegate_base_unittest.cc b/browser/net/brave_network_delegate_base_unittest.cc new file mode 100644 index 000000000000..4a912f67d2a2 --- /dev/null +++ b/browser/net/brave_network_delegate_base_unittest.cc @@ -0,0 +1,117 @@ +/* Copyright (c) 2019 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/net/brave_network_delegate_base.h" + +#include + +#include "brave/browser/net/url_context.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "net/url_request/url_request_test_util.h" +#include "url/gurl.h" + +using net::HttpResponseHeaders; + +namespace { + +const char kFirstPartyDomain[] = "http://firstparty.com/"; +const char kThirdPartyDomain[] = "http://thirdparty.com/"; +const char kAcceptLanguageHeader[] = "Accept-Language"; +const char kXSSProtectionHeader[] = "X-XSS-Protection"; + +const char kRawHeaders[] = + "HTTP/1.0 200 OK\n" + "Strict-Transport-Security: max-age=31557600\n" + "Accept-Language: *\n" + "Expect-CT: max-age=86400, enforce " + "report-uri=\"https://foo.example/report\"\n" + "Public-Key-Pins:" + "pin-sha256=\"cUPcTAZWKaASuYWhhBAkE3h2+soZS7sWs=\"" + "max-age=5184000; includeSubDomains\n" + "Public-Key-Pins-Report-Only:" + "pin-sha256=\"cUPcTAZWKaASuYWhhBAkE3h2+soZS7sWs=\"" + "max-age=5184000; includeSubDomains" + "report-uri=\"https://www.pkp.org/hpkp-report\"\n" + "X-XSS-Protection: 0"; + +class BraveNetworkDelegateBaseTest : public testing::Test { + public: + BraveNetworkDelegateBaseTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP), + context_(new net::TestURLRequestContext(true)) {} + ~BraveNetworkDelegateBaseTest() override {} + void SetUp() override { context_->Init(); } + net::TestURLRequestContext* context() { return context_.get(); } + + private: + content::TestBrowserThreadBundle thread_bundle_; + std::unique_ptr context_; +}; + +TEST_F(BraveNetworkDelegateBaseTest, RemoveTrackableSecurityHeaders) { + net::TestDelegate test_delegate; + GURL request_url(kThirdPartyDomain); + GURL tab_url(kFirstPartyDomain); + std::unique_ptr request = context()->CreateRequest( + request_url, net::IDLE, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); + + request->set_top_frame_origin(url::Origin::Create(tab_url)); + + scoped_refptr headers( + new HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders( + kRawHeaders, strnlen(kRawHeaders, sizeof kRawHeaders)))); + + RemoveTrackableSecurityHeadersForThirdParty(request.get(), nullptr, &headers); + for (auto header : *TrackableSecurityHeaders()) { + EXPECT_FALSE(headers->HasHeader(header.as_string())); + } + EXPECT_TRUE(headers->HasHeader(kAcceptLanguageHeader)); + EXPECT_TRUE(headers->HasHeader(kXSSProtectionHeader)); +} + +TEST_F(BraveNetworkDelegateBaseTest, RemoveTrackableSecurityHeadersMixedCase) { + net::TestDelegate test_delegate; + GURL request_url(kThirdPartyDomain); + GURL tab_url(kFirstPartyDomain); + std::unique_ptr request = context()->CreateRequest( + request_url, net::IDLE, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); + + request->set_top_frame_origin(url::Origin::Create(tab_url)); + + scoped_refptr headers( + new HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders( + kRawHeaders, strnlen(kRawHeaders, sizeof kRawHeaders)))); + + RemoveTrackableSecurityHeadersForThirdParty(request.get(), nullptr, &headers); + for (auto header : *TrackableSecurityHeaders()) { + EXPECT_FALSE(headers->HasHeader(header.as_string())); + } + EXPECT_TRUE(headers->HasHeader(kAcceptLanguageHeader)); + EXPECT_TRUE(headers->HasHeader(kXSSProtectionHeader)); +} + +TEST_F(BraveNetworkDelegateBaseTest, RetainTrackableSecurityHeaders) { + net::TestDelegate test_delegate; + GURL request_url(kFirstPartyDomain); + GURL tab_url(kFirstPartyDomain); + std::unique_ptr request = context()->CreateRequest( + request_url, net::IDLE, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); + + request->set_top_frame_origin(url::Origin::Create(tab_url)); + + scoped_refptr headers( + new HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders( + kRawHeaders, strnlen(kRawHeaders, sizeof kRawHeaders)))); + + RemoveTrackableSecurityHeadersForThirdParty(request.get(), nullptr, &headers); + for (auto header : *TrackableSecurityHeaders()) { + EXPECT_TRUE(headers->HasHeader(header.as_string())); + } + EXPECT_TRUE(headers->HasHeader(kAcceptLanguageHeader)); + EXPECT_TRUE(headers->HasHeader(kXSSProtectionHeader)); +} + +} // namespace diff --git a/browser/net/brave_network_delegate_hsts_fingerprinting_browsertest.cc b/browser/net/brave_network_delegate_hsts_fingerprinting_browsertest.cc new file mode 100644 index 000000000000..3471830f2d08 --- /dev/null +++ b/browser/net/brave_network_delegate_hsts_fingerprinting_browsertest.cc @@ -0,0 +1,103 @@ +/* Copyright (c) 2019 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/path_service.h" +#include "brave/common/brave_paths.h" +#include "brave/components/brave_shields/common/brave_shield_constants.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/test/browser_test_utils.h" +#include "net/dns/mock_host_resolver.h" + +using content::NavigationHandle; +using content::WebContents; +using content::WebContentsObserver; + +class RedirectObserver : public WebContentsObserver { + public: + explicit RedirectObserver(WebContents* web_contents) + : WebContentsObserver(web_contents) {} + ~RedirectObserver() override = default; + + void DidFinishNavigation(NavigationHandle* handle) override { + const net::HttpResponseHeaders* response = handle->GetResponseHeaders(); + if (response) { + const bool has_sts_header = + response->HasHeader("Strict-Transport-Security"); + sts_header_for_url_.insert( + std::pair(handle->GetURL(), has_sts_header)); + } + } + + bool has_sts_header(GURL url) const { + auto iter = sts_header_for_url_.find(url); + DCHECK(iter != sts_header_for_url_.end()); + return iter->second; + } + + private: + std::map sts_header_for_url_; + + DISALLOW_COPY_AND_ASSIGN(RedirectObserver); +}; + +class BraveNetworkDelegateBaseBrowserTest : public InProcessBrowserTest { + public: + void SetUpOnMainThread() override { + InProcessBrowserTest::SetUpOnMainThread(); + host_resolver()->AddRule("*", "127.0.0.1"); + + brave::RegisterPathProvider(); + base::FilePath test_data_dir; + base::PathService::Get(brave::DIR_TEST_DATA, &test_data_dir); + embedded_test_server()->ServeFilesFromDirectory(test_data_dir); + + ASSERT_TRUE(embedded_test_server()->Start()); + first_party_pattern_ = ContentSettingsPattern::FromString("http://a.com/*"); + iframe_pattern_ = ContentSettingsPattern::FromString("http://c.com/*"); + } + + content::WebContents* active_contents() { + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + HostContentSettingsMap* content_settings() { + return HostContentSettingsMapFactory::GetForProfile(browser()->profile()); + } + + private: + ContentSettingsPattern first_party_pattern_; + ContentSettingsPattern iframe_pattern_; +}; + +IN_PROC_BROWSER_TEST_F(BraveNetworkDelegateBaseBrowserTest, FirstPartySTS) { + const GURL third_party = + embedded_test_server()->GetURL("c.com", "/iframe_hsts.html"); + + RedirectObserver redirect_observer(active_contents()); + ui_test_utils::NavigateToURL(browser(), third_party); + + EXPECT_TRUE(redirect_observer.has_sts_header(third_party)); +} + +IN_PROC_BROWSER_TEST_F(BraveNetworkDelegateBaseBrowserTest, ThirdPartySTS) { + const GURL third_party = + embedded_test_server()->GetURL("c.com", "/iframe_hsts.html"); + const GURL first_party = + embedded_test_server()->GetURL("a.com", "/hsts.html"); + + RedirectObserver redirect_observer(active_contents()); + ui_test_utils::NavigateToURL(browser(), first_party); + + EXPECT_FALSE(redirect_observer.has_sts_header(third_party)); +} diff --git a/test/BUILD.gn b/test/BUILD.gn index 83e26573050b..5a540978e160 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -49,6 +49,7 @@ test("brave_unit_tests") { "//brave/browser/net/brave_ad_block_tp_network_delegate_helper_unittest.cc", "//brave/browser/net/brave_common_static_redirect_network_delegate_helper_unittest.cc", "//brave/browser/net/brave_httpse_network_delegate_helper_unittest.cc", + "//brave/browser/net/brave_network_delegate_base_unittest.cc", "//brave/browser/net/brave_referrals_network_delegate_helper_unittest.cc", "//brave/browser/net/brave_site_hacks_network_delegate_helper_unittest.cc", "//brave/browser/net/brave_static_redirect_network_delegate_helper_unittest.cc", @@ -284,6 +285,7 @@ test("brave_browser_tests") { "//brave/browser/extensions/api/brave_shields_api_browsertest.cc", "//brave/browser/extensions/api/brave_theme_api_browsertest.cc", "//brave/browser/net/brave_network_delegate_browsertest.cc", + "//brave/browser/net/brave_network_delegate_hsts_fingerprinting_browsertest.cc", "//brave/browser/renderer_context_menu/brave_mock_render_view_context_menu.cc", "//brave/browser/renderer_context_menu/brave_mock_render_view_context_menu.h", "//brave/browser/renderer_context_menu/brave_spelling_menu_observer_browsertest.cc", diff --git a/test/data/hsts.html b/test/data/hsts.html new file mode 100644 index 000000000000..6e727912232f --- /dev/null +++ b/test/data/hsts.html @@ -0,0 +1,5 @@ +Third Party HSTS + + + + diff --git a/test/data/iframe_hsts.html b/test/data/iframe_hsts.html new file mode 100644 index 000000000000..980a0d5f19a6 --- /dev/null +++ b/test/data/iframe_hsts.html @@ -0,0 +1 @@ +Hello World! diff --git a/test/data/iframe_hsts.html.mock-http-headers b/test/data/iframe_hsts.html.mock-http-headers new file mode 100644 index 000000000000..e3375d5d1597 --- /dev/null +++ b/test/data/iframe_hsts.html.mock-http-headers @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +Cache-Control: private +Content-Type: text/html; charset=ISO-8859-1 +Strict-Transport-Security: max-age=123; includeSubdomains