Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 3419: Mitigate HSTS fingerprinting #1744

Merged
merged 2 commits into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions browser/net/brave_network_delegate_base.cc
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <utility>

#include "base/task/post_task.h"
#include "brave/common/pref_names.h"
Expand All @@ -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 {
Expand All @@ -40,6 +44,41 @@ content::WebContents* GetWebContentsFromProcessAndFrameId(int render_process_id,

} // namespace

base::flat_set<base::StringPiece>* TrackableSecurityHeaders() {
static base::NoDestructor<base::flat_set<base::StringPiece>>
kTrackableSecurityHeaders(base::flat_set<base::StringPiece>{
"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<net::HttpResponseHeaders>* 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) {
Expand Down Expand Up @@ -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()));
}
}

Expand Down Expand Up @@ -124,6 +162,9 @@ int BraveNetworkDelegateBase::OnHeadersReceived(
const net::HttpResponseHeaders* original_response_headers,
scoped_refptr<net::HttpResponseHeaders>* 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,
Expand Down
20 changes: 18 additions & 2 deletions browser/net/brave_network_delegate_base.h
Original file line number Diff line number Diff line change
@@ -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 <map>
#include <memory>
#include <string>
#include <vector>

#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"
Expand All @@ -20,6 +28,13 @@ namespace net {
class URLRequest;
}

base::flat_set<base::StringPiece>* TrackableSecurityHeaders();

void RemoveTrackableSecurityHeadersForThirdParty(
net::URLRequest* request,
const net::HttpResponseHeaders* original_response_headers,
scoped_refptr<net::HttpResponseHeaders>* override_response_headers);

// BraveNetworkDelegateBase is the central point from within the Brave code to
// add hooks into the network stack.
class BraveNetworkDelegateBase : public ChromeNetworkDelegate {
Expand All @@ -28,7 +43,8 @@ class BraveNetworkDelegateBase : public ChromeNetworkDelegate {
using ResponseListener = base::Callback<void(const base::DictionaryValue&,
const ResponseCallback&)>;

BraveNetworkDelegateBase(extensions::EventRouterForwarder* event_router);
explicit BraveNetworkDelegateBase(
extensions::EventRouterForwarder* event_router);
~BraveNetworkDelegateBase() override;

bool IsRequestIdentifierValid(uint64_t request_identifier);
Expand Down
117 changes: 117 additions & 0 deletions browser/net/brave_network_delegate_base_unittest.cc
Original file line number Diff line number Diff line change
@@ -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 <string>

#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<net::TestURLRequestContext> context_;
};

TEST_F(BraveNetworkDelegateBaseTest, RemoveTrackableSecurityHeaders) {
net::TestDelegate test_delegate;
GURL request_url(kThirdPartyDomain);
GURL tab_url(kFirstPartyDomain);
std::unique_ptr<net::URLRequest> 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<HttpResponseHeaders> 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()));
}
diracdeltas marked this conversation as resolved.
Show resolved Hide resolved
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<net::URLRequest> 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<HttpResponseHeaders> 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<net::URLRequest> 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<HttpResponseHeaders> 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
Original file line number Diff line number Diff line change
@@ -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<GURL, bool>(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<GURL, bool> 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));
}
2 changes: 2 additions & 0 deletions test/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions test/data/hsts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html><head><title>Third Party HSTS</title></head>
<body>
<iframe src="/cross-site/c.com/iframe_hsts.html"></iframe>
</body></html>

1 change: 1 addition & 0 deletions test/data/iframe_hsts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World!
4 changes: 4 additions & 0 deletions test/data/iframe_hsts.html.mock-http-headers
Original file line number Diff line number Diff line change
@@ -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