blob: 3c862e65bb871054eb4cd5d703421f1b820edf12 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/safe_browsing_db/database_manager.h"
#include <stddef.h>
#include <set>
#include <string>
#include <vector>
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/safe_browsing_db/test_database_manager.h"
#include "components/safe_browsing_db/v4_get_hash_protocol_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace safe_browsing {
namespace {
void InvokeFullHashCallback(
V4GetHashProtocolManager::FullHashCallback callback,
const std::vector<SBFullHashResult>& full_hashes,
base::Time negative_cache_expire) {
callback.Run(full_hashes, negative_cache_expire);
}
// A TestV4GetHashProtocolManager that returns fixed responses from the
// Safe Browsing server for testing purpose.
class TestV4GetHashProtocolManager : public V4GetHashProtocolManager {
public:
TestV4GetHashProtocolManager(
net::URLRequestContextGetter* request_context_getter,
const V4ProtocolConfig& config)
: V4GetHashProtocolManager(request_context_getter, config),
negative_cache_expire_(base::Time()), delay_seconds_(0) {}
~TestV4GetHashProtocolManager() override {}
void GetFullHashesWithApis(const std::vector<SBPrefix>& prefixes,
FullHashCallback callback) override {
prefixes_ = prefixes;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(InvokeFullHashCallback, callback, full_hashes_,
negative_cache_expire_),
base::TimeDelta::FromSeconds(delay_seconds_));
}
void SetDelaySeconds(int delay) {
delay_seconds_ = delay;
}
void SetNegativeCacheDurationMins(base::Time now,
int negative_cache_duration_mins) {
// Don't add a TimeDelta to the maximum time to avoid undefined behavior.
negative_cache_expire_ = now.is_max() ? now :
now + base::TimeDelta::FromMinutes(negative_cache_duration_mins);
}
// Prepare the GetFullHash results for the next request.
void AddGetFullHashResponse(const SBFullHashResult& full_hash_result) {
full_hashes_.push_back(full_hash_result);
}
// Clear the GetFullHash results for the next request.
void ClearFullHashResponse() {
full_hashes_.clear();
}
// Returns the prefixes that were sent in the last request.
const std::vector<SBPrefix>& GetRequestPrefixes() { return prefixes_; }
private:
std::vector<SBPrefix> prefixes_;
std::vector<SBFullHashResult> full_hashes_;
base::Time negative_cache_expire_;
int delay_seconds_;
};
// Factory that creates test protocol manager instances.
class TestV4GetHashProtocolManagerFactory :
public V4GetHashProtocolManagerFactory {
public:
TestV4GetHashProtocolManagerFactory() {}
~TestV4GetHashProtocolManagerFactory() override {}
V4GetHashProtocolManager* CreateProtocolManager(
net::URLRequestContextGetter* request_context_getter,
const V4ProtocolConfig& config) override {
return new TestV4GetHashProtocolManager(request_context_getter, config);
}
};
class TestClient : public SafeBrowsingDatabaseManager::Client {
public:
TestClient() : callback_invoked_(false) {}
~TestClient() override {}
void OnCheckApiBlacklistUrlResult(const GURL& url,
const ThreatMetadata& metadata) override {
blocked_permissions_ = metadata.api_permissions;
callback_invoked_ = true;
}
const std::set<std::string>& GetBlockedPermissions() {
return blocked_permissions_;
}
bool callback_invoked() {return callback_invoked_;}
private:
std::set<std::string> blocked_permissions_;
bool callback_invoked_;
DISALLOW_COPY_AND_ASSIGN(TestClient);
};
} // namespace
class SafeBrowsingDatabaseManagerTest : public testing::Test {
protected:
void SetUp() override {
V4GetHashProtocolManager::RegisterFactory(
base::WrapUnique(new TestV4GetHashProtocolManagerFactory()));
db_manager_ = new TestSafeBrowsingDatabaseManager();
db_manager_->StartOnIOThread(NULL, V4ProtocolConfig());
}
void TearDown() override {
base::RunLoop().RunUntilIdle();
db_manager_->StopOnIOThread(false);
V4GetHashProtocolManager::RegisterFactory(nullptr);
}
scoped_refptr<SafeBrowsingDatabaseManager> db_manager_;
private:
content::TestBrowserThreadBundle test_browser_thread_bundle_;
};
TEST_F(SafeBrowsingDatabaseManagerTest, CheckApiBlacklistUrlWrongScheme) {
TestClient client;
const GURL url("file://example.txt");
EXPECT_TRUE(db_manager_->CheckApiBlacklistUrl(url, &client));
}
TEST_F(SafeBrowsingDatabaseManagerTest, CheckApiBlacklistUrlPrefixes) {
TestClient client;
const GURL url("https://www.example.com/more");
// Generated from the sorted output of UrlToFullHashes in util.h.
std::vector<SBPrefix> expected_prefixes =
{1237562338, 2871045197, 3553205461, 3766933875};
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
std::vector<SBPrefix> prefixes = pm->GetRequestPrefixes();
EXPECT_EQ(expected_prefixes.size(), prefixes.size());
for (unsigned int i = 0; i < prefixes.size(); ++i) {
EXPECT_EQ(expected_prefixes[i], prefixes[i]);
}
}
TEST_F(SafeBrowsingDatabaseManagerTest, HandleGetHashesWithApisResults) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
pm->AddGetFullHashResponse(full_hash_result);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(1ul, permissions.size());
EXPECT_EQ(1ul, permissions.count("GEOLOCATION"));
}
TEST_F(SafeBrowsingDatabaseManagerTest, HandleGetHashesWithApisResultsNoMatch) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("wrongexample.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
pm->AddGetFullHashResponse(full_hash_result);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(0ul, permissions.size());
}
TEST_F(SafeBrowsingDatabaseManagerTest, HandleGetHashesWithApisResultsMatches) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
pm->AddGetFullHashResponse(full_hash_result);
SBFullHashResult full_hash_result2;
full_hash_result2.hash = SBFullHashForString("example.com/more");
full_hash_result2.metadata.api_permissions.insert("NOTIFICATIONS");
pm->AddGetFullHashResponse(full_hash_result2);
SBFullHashResult full_hash_result3;
full_hash_result3.hash = SBFullHashForString("wrongexample.com/");
full_hash_result3.metadata.api_permissions.insert("AUDIO_CAPTURE");
pm->AddGetFullHashResponse(full_hash_result3);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(2ul, permissions.size());
EXPECT_EQ(1ul, permissions.count("GEOLOCATION"));
EXPECT_EQ(1ul, permissions.count("NOTIFICATIONS"));
}
TEST_F(SafeBrowsingDatabaseManagerTest, CancelApiCheck) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
pm->AddGetFullHashResponse(full_hash_result);
pm->SetDelaySeconds(100);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
EXPECT_TRUE(db_manager_->CancelApiCheck(&client));
base::RunLoop().RunUntilIdle();
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(0ul, permissions.size());
EXPECT_FALSE(client.callback_invoked());
}
TEST_F(SafeBrowsingDatabaseManagerTest, ResultsAreCached) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
base::Time now = base::Time::UnixEpoch();
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
full_hash_result.cache_expire_after = now + base::TimeDelta::FromMinutes(3);
pm->AddGetFullHashResponse(full_hash_result);
pm->SetNegativeCacheDurationMins(now, 5);
EXPECT_TRUE(db_manager_->v4_full_hash_cache_.empty());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(1ul, permissions.size());
EXPECT_EQ(1ul, permissions.count("GEOLOCATION"));
// Check the cache.
const SafeBrowsingDatabaseManager::PrefixToFullHashResultsMap& cache =
db_manager_->v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE];
// Generated from the sorted output of UrlToFullHashes in util.h.
std::vector<SBPrefix> expected_prefixes =
{1237562338, 2871045197, 3553205461, 3766933875};
EXPECT_EQ(expected_prefixes.size(),
db_manager_->v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE].size());
auto entry = cache.find(expected_prefixes[0]);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(now + base::TimeDelta::FromMinutes(5), entry->second.expire_after);
EXPECT_EQ(0ul, entry->second.full_hashes.size());
entry = cache.find(expected_prefixes[1]);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(now + base::TimeDelta::FromMinutes(5), entry->second.expire_after);
EXPECT_EQ(0ul, entry->second.full_hashes.size());
entry = cache.find(expected_prefixes[2]);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(now + base::TimeDelta::FromMinutes(5), entry->second.expire_after);
EXPECT_EQ(0ul, entry->second.full_hashes.size());
entry = cache.find(expected_prefixes[3]);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(now + base::TimeDelta::FromMinutes(5), entry->second.expire_after);
EXPECT_EQ(1ul, entry->second.full_hashes.size());
EXPECT_TRUE(SBFullHashEqual(full_hash_result.hash,
entry->second.full_hashes[0].hash));
EXPECT_EQ(1ul, entry->second.full_hashes[0].metadata.api_permissions.size());
EXPECT_EQ(1ul, entry->second.full_hashes[0].metadata.api_permissions.
count("GEOLOCATION"));
EXPECT_EQ(full_hash_result.cache_expire_after,
entry->second.full_hashes[0].cache_expire_after);
}
// An uninitialized value for negative cache expire does not cache results.
TEST_F(SafeBrowsingDatabaseManagerTest, ResultsAreNotCachedOnNull) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
base::Time now = base::Time::UnixEpoch();
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.cache_expire_after = now + base::TimeDelta::FromMinutes(3);
pm->AddGetFullHashResponse(full_hash_result);
EXPECT_TRUE(db_manager_->v4_full_hash_cache_.empty());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
EXPECT_TRUE(
db_manager_->v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE].empty());
}
// Checks that results are looked up correctly in the cache.
TEST_F(SafeBrowsingDatabaseManagerTest, GetCachedResults) {
base::Time now = base::Time::UnixEpoch();
std::vector<SBFullHash> full_hashes;
SBFullHash full_hash = SBFullHashForString("example.com/");
full_hashes.push_back(full_hash);
std::vector<SBFullHashResult> cached_results;
std::vector<SBPrefix> prefixes;
db_manager_->GetFullHashCachedResults(SB_THREAT_TYPE_API_ABUSE,
full_hashes, now, &prefixes, &cached_results);
// The cache is empty.
EXPECT_TRUE(cached_results.empty());
EXPECT_EQ(1ul, prefixes.size());
EXPECT_EQ(full_hash.prefix, prefixes[0]);
// Prefix has a cache entry but full hash is not there.
SBCachedFullHashResult& entry = db_manager_->
v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE][full_hash.prefix] =
SBCachedFullHashResult(now + base::TimeDelta::FromMinutes(5));
db_manager_->GetFullHashCachedResults(SB_THREAT_TYPE_API_ABUSE,
full_hashes, now, &prefixes, &cached_results);
EXPECT_TRUE(prefixes.empty());
EXPECT_TRUE(cached_results.empty());
// Expired negative cache entry.
entry.expire_after = now - base::TimeDelta::FromMinutes(5);
db_manager_->GetFullHashCachedResults(SB_THREAT_TYPE_API_ABUSE,
full_hashes, now, &prefixes, &cached_results);
EXPECT_TRUE(cached_results.empty());
EXPECT_EQ(1ul, prefixes.size());
EXPECT_EQ(full_hash.prefix, prefixes[0]);
// Now put the full hash in the cache.
SBFullHashResult full_hash_result;
full_hash_result.hash = full_hash;
full_hash_result.cache_expire_after = now + base::TimeDelta::FromMinutes(3);
entry.full_hashes.push_back(full_hash_result);
db_manager_->GetFullHashCachedResults(SB_THREAT_TYPE_API_ABUSE,
full_hashes, now, &prefixes, &cached_results);
EXPECT_TRUE(prefixes.empty());
EXPECT_EQ(1ul, cached_results.size());
EXPECT_TRUE(SBFullHashEqual(full_hash, cached_results[0].hash));
// Expired full hash in cache.
entry.full_hashes.clear();
full_hash_result.cache_expire_after = now - base::TimeDelta::FromMinutes(3);
entry.full_hashes.push_back(full_hash_result);
db_manager_->GetFullHashCachedResults(SB_THREAT_TYPE_API_ABUSE,
full_hashes, now, &prefixes, &cached_results);
EXPECT_TRUE(cached_results.empty());
EXPECT_EQ(1ul, prefixes.size());
EXPECT_EQ(full_hash.prefix, prefixes[0]);
}
// Checks that the cached results and request results are merged.
TEST_F(SafeBrowsingDatabaseManagerTest, CachedResultsMerged) {
TestClient client;
const GURL url("https://www.example.com/more");
TestV4GetHashProtocolManager* pm = static_cast<TestV4GetHashProtocolManager*>(
db_manager_->v4_get_hash_protocol_manager_);
// Set now to max time so the cache expire times are in the future.
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.metadata.api_permissions.insert("GEOLOCATION");
full_hash_result.cache_expire_after = base::Time::Max();
pm->AddGetFullHashResponse(full_hash_result);
pm->SetNegativeCacheDurationMins(base::Time::Max(), 0);
EXPECT_TRUE(db_manager_->v4_full_hash_cache_.empty());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client.callback_invoked());
const std::set<std::string>& permissions = client.GetBlockedPermissions();
EXPECT_EQ(1ul, permissions.size());
EXPECT_EQ(1ul, permissions.count("GEOLOCATION"));
// The results should be cached, so remove them from the protocol manager
// response.
TestClient client2;
pm->ClearFullHashResponse();
pm->SetNegativeCacheDurationMins(base::Time(), 0);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client2));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client2.callback_invoked());
const std::set<std::string>& permissions2 =
client2.GetBlockedPermissions();
EXPECT_EQ(1ul, permissions2.size());
EXPECT_EQ(1ul, permissions2.count("GEOLOCATION"));
// Add a different result to the protocol manager response and ensure it is
// merged with the cached result in the metadata.
TestClient client3;
const GURL url2("https://m.example.com/more");
full_hash_result.hash = SBFullHashForString("m.example.com/");
full_hash_result.metadata.api_permissions.insert("NOTIFICATIONS");
pm->AddGetFullHashResponse(full_hash_result);
pm->SetNegativeCacheDurationMins(base::Time::Max(), 0);
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url2, &client3));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(client3.callback_invoked());
const std::set<std::string>& permissions3 =
client3.GetBlockedPermissions();
EXPECT_EQ(2ul, permissions3.size());
EXPECT_EQ(1ul, permissions3.count("GEOLOCATION"));
EXPECT_EQ(1ul, permissions3.count("NOTIFICATIONS"));
}
TEST_F(SafeBrowsingDatabaseManagerTest, CachedResultsAreEvicted) {
base::Time epoch = base::Time::UnixEpoch();
SBFullHashResult full_hash_result;
full_hash_result.hash = SBFullHashForString("example.com/");
full_hash_result.cache_expire_after = epoch;
SafeBrowsingDatabaseManager::PrefixToFullHashResultsMap& cache =
db_manager_->v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE];
// Fill the cache with some expired entries.
// Both negative cache and full hash expired.
cache[full_hash_result.hash.prefix] = SBCachedFullHashResult(epoch);
cache[full_hash_result.hash.prefix].full_hashes.push_back(full_hash_result);
TestClient client;
const GURL url("https://www.example.com/more");
EXPECT_EQ(1ul, cache.size());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
// Cache should be empty.
EXPECT_TRUE(client.callback_invoked());
EXPECT_TRUE(cache.empty());
// Negative cache still valid and full hash expired.
cache[full_hash_result.hash.prefix] =
SBCachedFullHashResult(base::Time::Max());
cache[full_hash_result.hash.prefix].full_hashes.push_back(full_hash_result);
EXPECT_EQ(1ul, cache.size());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
// Cache entry should still be there.
EXPECT_EQ(1ul, cache.size());
auto entry = cache.find(full_hash_result.hash.prefix);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(base::Time::Max(), entry->second.expire_after);
EXPECT_EQ(1ul, entry->second.full_hashes.size());
EXPECT_TRUE(SBFullHashEqual(full_hash_result.hash,
entry->second.full_hashes[0].hash));
EXPECT_EQ(full_hash_result.cache_expire_after,
entry->second.full_hashes[0].cache_expire_after);
// Negative cache still valid and full hash still valid.
cache[full_hash_result.hash.prefix].full_hashes[0].
cache_expire_after = base::Time::Max();
EXPECT_EQ(1ul, cache.size());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
// Cache entry should still be there.
EXPECT_EQ(1ul, cache.size());
entry = cache.find(full_hash_result.hash.prefix);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(base::Time::Max(), entry->second.expire_after);
EXPECT_EQ(1ul, entry->second.full_hashes.size());
EXPECT_TRUE(SBFullHashEqual(full_hash_result.hash,
entry->second.full_hashes[0].hash));
EXPECT_EQ(base::Time::Max(),
entry->second.full_hashes[0].cache_expire_after);
// Negative cache expired and full hash still valid.
cache[full_hash_result.hash.prefix].expire_after = epoch;
EXPECT_EQ(1ul, cache.size());
EXPECT_FALSE(db_manager_->CheckApiBlacklistUrl(url, &client));
base::RunLoop().RunUntilIdle();
// Cache entry should still be there.
EXPECT_EQ(1ul, cache.size());
entry = cache.find(full_hash_result.hash.prefix);
EXPECT_NE(cache.end(), entry);
EXPECT_EQ(epoch, entry->second.expire_after);
EXPECT_EQ(1ul, entry->second.full_hashes.size());
EXPECT_TRUE(SBFullHashEqual(full_hash_result.hash,
entry->second.full_hashes[0].hash));
EXPECT_EQ(base::Time::Max(),
entry->second.full_hashes[0].cache_expire_after);
}
} // namespace safe_browsing