quarantine_win_unittest.cc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. // Copyright 2016 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include <windows.h>
  5. #include <wininet.h>
  6. #include "base/bind.h"
  7. #include "base/files/file_path.h"
  8. #include "base/files/file_util.h"
  9. #include "base/files/scoped_temp_dir.h"
  10. #include "base/run_loop.h"
  11. #include "base/strings/stringprintf.h"
  12. #include "base/strings/utf_string_conversions.h"
  13. #include "base/test/task_environment.h"
  14. #include "base/test/test_file_util.h"
  15. #include "base/test/test_reg_util_win.h"
  16. #include "base/win/scoped_com_initializer.h"
  17. #include "base/win/win_util.h"
  18. #include "base/win/windows_version.h"
  19. #include "components/services/quarantine/quarantine.h"
  20. #include "components/services/quarantine/test_support.h"
  21. #include "net/base/filename_util.h"
  22. #include "testing/gtest/include/gtest/gtest.h"
  23. #include "url/gurl.h"
  24. namespace quarantine {
  25. namespace {
  26. const char kDummySourceUrl[] = "https://example.com/foo";
  27. const char kDummyReferrerUrl[] = "https://example.com/referrer";
  28. const char kDummyClientGuid[] = "A1B69307-8FA2-4B6F-9181-EA06051A48A7";
  29. const char* const kUntrustedURLs[] = {
  30. "http://example.com/foo",
  31. "https://example.com/foo",
  32. "ftp://example.com/foo",
  33. "ftp://example.com:2121/foo",
  34. "data:text/plain,Hello%20world",
  35. "blob:https://example.com/126278b3-58f3-4b4a-a914-1d1185d634f6",
  36. "about:internet",
  37. ""};
  38. // Creates a non-empty file at |file_path|.
  39. bool CreateFile(const base::FilePath& file_path) {
  40. constexpr char kTestData[] = "Hello world!";
  41. return base::WriteFile(file_path, kTestData, std::size(kTestData)) ==
  42. static_cast<int>(std::size(kTestData));
  43. }
  44. base::FilePath GetZoneIdentifierStreamPath(const base::FilePath& file_path) {
  45. const base::FilePath::CharType kMotwStreamSuffix[] =
  46. FILE_PATH_LITERAL(":Zone.Identifier");
  47. return base::FilePath(file_path.value() + kMotwStreamSuffix);
  48. }
  49. // Reads the Zone.Identifier alternate data stream from |file_path| into
  50. // |contents|.
  51. bool GetZoneIdentifierStreamContents(const base::FilePath& file_path,
  52. std::string* contents) {
  53. DCHECK(contents);
  54. return base::ReadFileToString(GetZoneIdentifierStreamPath(file_path),
  55. contents);
  56. }
  57. // Maps a domain and protocol to a zone.
  58. class ScopedZoneForSite {
  59. public:
  60. enum ZoneIdentifierType : DWORD {
  61. kMyComputer = 0,
  62. kLocalIntranetZone = 1,
  63. kTrustedSitesZone = 2,
  64. kInternetZone = 3,
  65. kRestrictedSitesZone = 4,
  66. };
  67. ScopedZoneForSite(const std::wstring& domain,
  68. const std::wstring& protocol,
  69. ZoneIdentifierType zone_identifier_type);
  70. ScopedZoneForSite(const ScopedZoneForSite&) = delete;
  71. ScopedZoneForSite& operator=(const ScopedZoneForSite&) = delete;
  72. ~ScopedZoneForSite();
  73. private:
  74. std::wstring domain_;
  75. std::wstring protocol_;
  76. };
  77. ScopedZoneForSite::ScopedZoneForSite(const std::wstring& domain,
  78. const std::wstring& protocol,
  79. ZoneIdentifierType zone_identifier_type)
  80. : domain_(domain), protocol_(protocol) {
  81. std::wstring registry_path = base::StringPrintf(
  82. L"Software\\Microsoft\\Windows\\CurrentVersion\\Internet "
  83. L"Settings\\ZoneMap\\Domains\\%ls",
  84. domain_.c_str());
  85. base::win::RegKey registry_key(HKEY_CURRENT_USER, registry_path.c_str(),
  86. KEY_SET_VALUE);
  87. EXPECT_EQ(registry_key.WriteValue(protocol_.c_str(), zone_identifier_type),
  88. ERROR_SUCCESS);
  89. }
  90. ScopedZoneForSite::~ScopedZoneForSite() {
  91. std::wstring registry_path = base::StringPrintf(
  92. L"Software\\Microsoft\\Windows\\CurrentVersion\\Internet "
  93. L"Settings\\ZoneMap\\Domains\\%ls",
  94. domain_.c_str());
  95. base::win::RegKey registry_key(HKEY_CURRENT_USER, registry_path.c_str(),
  96. KEY_SET_VALUE);
  97. registry_key.DeleteValue(protocol_.c_str());
  98. }
  99. // Sets the internet Zone.Identifier alternate data stream for |file_path|.
  100. bool AddInternetZoneIdentifierDirectly(const base::FilePath& file_path) {
  101. static const char kMotwForInternetZone[] = "[ZoneTransfer]\r\nZoneId=3\r\n";
  102. return base::WriteFile(GetZoneIdentifierStreamPath(file_path),
  103. kMotwForInternetZone,
  104. std::size(kMotwForInternetZone)) ==
  105. static_cast<int>(std::size(kMotwForInternetZone));
  106. }
  107. void CheckQuarantineResult(QuarantineFileResult result,
  108. QuarantineFileResult expected_result) {
  109. EXPECT_EQ(expected_result, result);
  110. }
  111. } // namespace
  112. class QuarantineWinTest : public ::testing::Test {
  113. public:
  114. QuarantineWinTest() = default;
  115. QuarantineWinTest(const QuarantineWinTest&) = delete;
  116. QuarantineWinTest& operator=(const QuarantineWinTest&) = delete;
  117. ~QuarantineWinTest() override = default;
  118. void SetUp() override {
  119. ASSERT_NO_FATAL_FAILURE(
  120. registry_override_.OverrideRegistry(HKEY_CURRENT_USER));
  121. ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
  122. scoped_zone_for_trusted_site_ = std::make_unique<ScopedZoneForSite>(
  123. GetTrustedSite(), L"https",
  124. ScopedZoneForSite::ZoneIdentifierType::kTrustedSitesZone);
  125. scoped_zone_for_restricted_site_ = std::make_unique<ScopedZoneForSite>(
  126. GetRestrictedSite(), L"https",
  127. ScopedZoneForSite::ZoneIdentifierType::kRestrictedSitesZone);
  128. scoped_zone_for_internet_site_ = std::make_unique<ScopedZoneForSite>(
  129. GetInternetSite(), L"https",
  130. ScopedZoneForSite::ZoneIdentifierType::kInternetZone);
  131. }
  132. base::FilePath GetTempDir() { return scoped_temp_dir_.GetPath(); }
  133. const wchar_t* GetTrustedSite() { return L"thisisatrustedsite.com"; }
  134. const wchar_t* GetRestrictedSite() { return L"thisisarestrictedsite.com"; }
  135. const wchar_t* GetInternetSite() { return L"example.com"; }
  136. private:
  137. base::test::SingleThreadTaskEnvironment task_environment_;
  138. registry_util::RegistryOverrideManager registry_override_;
  139. base::ScopedTempDir scoped_temp_dir_;
  140. base::win::ScopedCOMInitializer com_initializer_{
  141. base::win::ScopedCOMInitializer::Uninitialization::kBlockPremature};
  142. // Due to caching, these sites zone must be set for all tests, so that the
  143. // order the tests are run does not matter.
  144. std::unique_ptr<ScopedZoneForSite> scoped_zone_for_trusted_site_;
  145. std::unique_ptr<ScopedZoneForSite> scoped_zone_for_internet_site_;
  146. std::unique_ptr<ScopedZoneForSite> scoped_zone_for_restricted_site_;
  147. };
  148. // If the file is missing, the QuarantineFile() call should return FILE_MISSING.
  149. TEST_F(QuarantineWinTest, MissingFile) {
  150. QuarantineFile(GetTempDir().AppendASCII("does-not-exist.exe"),
  151. GURL(kDummySourceUrl), GURL(kDummyReferrerUrl),
  152. kDummyClientGuid,
  153. base::BindOnce(&CheckQuarantineResult,
  154. QuarantineFileResult::FILE_MISSING));
  155. base::RunLoop().RunUntilIdle();
  156. }
  157. // On Windows systems, files downloaded from a local source are considered
  158. // trustworthy. Hence they aren't annotated with source information. This test
  159. // verifies this behavior since the other tests in this suite would pass with a
  160. // false positive if local files are being annotated with the MOTW for the
  161. // internet zone.
  162. TEST_F(QuarantineWinTest, LocalFile_DependsOnLocalConfig) {
  163. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  164. const char* const kLocalSourceURLs[] = {"http://localhost/foo",
  165. "file:///C:/some-local-dir/foo.exe"};
  166. for (const char* source_url : kLocalSourceURLs) {
  167. SCOPED_TRACE(::testing::Message() << "Trying URL " << source_url);
  168. ASSERT_TRUE(CreateFile(test_file));
  169. QuarantineFile(
  170. test_file, GURL(source_url), GURL(), kDummyClientGuid,
  171. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  172. base::RunLoop().RunUntilIdle();
  173. std::string zone_identifier;
  174. GetZoneIdentifierStreamContents(test_file, &zone_identifier);
  175. // No zone identifier for local source.
  176. EXPECT_TRUE(zone_identifier.empty());
  177. ASSERT_TRUE(base::DeleteFile(test_file));
  178. }
  179. }
  180. // A file downloaded from the internet should be annotated with .. something.
  181. // The specific zone assigned to our dummy source URL depends on the local
  182. // configuration. But no sane configuration should be treating the dummy URL as
  183. // a trusted source for anything.
  184. TEST_F(QuarantineWinTest, DownloadedFile_DependsOnLocalConfig) {
  185. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  186. for (const char* source_url : kUntrustedURLs) {
  187. SCOPED_TRACE(::testing::Message() << "Trying URL " << source_url);
  188. ASSERT_TRUE(CreateFile(test_file));
  189. QuarantineFile(
  190. test_file, GURL(source_url), GURL(), kDummyClientGuid,
  191. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  192. base::RunLoop().RunUntilIdle();
  193. std::string zone_identifier;
  194. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  195. // The actual assigned zone could be anything and the contents of the zone
  196. // identifier depends on the version of Windows. So only testing that there
  197. // is a zone annotation.
  198. EXPECT_FALSE(zone_identifier.empty());
  199. ASSERT_TRUE(base::DeleteFile(test_file));
  200. }
  201. }
  202. TEST_F(QuarantineWinTest, UnsafeReferrer_DependsOnLocalConfig) {
  203. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  204. std::vector<std::string> unsafe_referrers(std::begin(kUntrustedURLs),
  205. std::end(kUntrustedURLs));
  206. // Add one more test case.
  207. std::string huge_referrer = "http://example.com/";
  208. huge_referrer.append(INTERNET_MAX_URL_LENGTH * 2, 'a');
  209. unsafe_referrers.push_back(huge_referrer);
  210. for (const auto& referrer_url : unsafe_referrers) {
  211. SCOPED_TRACE(::testing::Message() << "Trying URL " << referrer_url);
  212. ASSERT_TRUE(CreateFile(test_file));
  213. QuarantineFile(
  214. test_file, GURL("http://example.com/good"), GURL(referrer_url),
  215. kDummyClientGuid,
  216. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  217. base::RunLoop().RunUntilIdle();
  218. std::string zone_identifier;
  219. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  220. // The actual assigned zone could be anything and the contents of the zone
  221. // identifier depends on the version of Windows. So only testing that there
  222. // is a zone annotation.
  223. EXPECT_FALSE(zone_identifier.empty());
  224. ASSERT_TRUE(base::DeleteFile(test_file));
  225. }
  226. }
  227. // An empty source URL should result in a file that's treated the same as one
  228. // downloaded from the internet.
  229. TEST_F(QuarantineWinTest, EmptySource_DependsOnLocalConfig) {
  230. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  231. ASSERT_TRUE(CreateFile(test_file));
  232. QuarantineFile(
  233. test_file, GURL(), GURL(), kDummyClientGuid,
  234. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  235. base::RunLoop().RunUntilIdle();
  236. std::string zone_identifier;
  237. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  238. // The actual assigned zone could be anything and the contents of the zone
  239. // identifier depends on the version of Windows. So only testing that there is
  240. // a zone annotation.
  241. EXPECT_FALSE(zone_identifier.empty());
  242. }
  243. // Empty files aren't passed to AVScanFile. They are instead marked manually. If
  244. // the file is passed to AVScanFile, then there wouldn't be a MOTW attached to
  245. // it and the test would fail.
  246. TEST_F(QuarantineWinTest, EmptyFile) {
  247. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  248. ASSERT_EQ(0, base::WriteFile(test_file, "", 0u));
  249. QuarantineFile(
  250. test_file, net::FilePathToFileURL(test_file), GURL(), kDummyClientGuid,
  251. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  252. base::RunLoop().RunUntilIdle();
  253. std::string zone_identifier;
  254. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  255. // The actual assigned zone could be anything and the contents of the zone
  256. // identifier depends on the version of Windows. So only testing that there is
  257. // a zone annotation.
  258. EXPECT_FALSE(zone_identifier.empty());
  259. }
  260. // If there is no client GUID supplied to the QuarantineFile() call, then rather
  261. // than invoking AVScanFile, the MOTW will be applied manually. If the file is
  262. // passed to AVScanFile, then there wouldn't be a MOTW attached to it and the
  263. // test would fail.
  264. TEST_F(QuarantineWinTest, NoClientGuid) {
  265. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  266. ASSERT_TRUE(CreateFile(test_file));
  267. QuarantineFile(
  268. test_file, net::FilePathToFileURL(test_file), GURL(), std::string(),
  269. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  270. base::RunLoop().RunUntilIdle();
  271. std::string zone_identifier;
  272. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  273. // The actual assigned zone could be anything and the contents of the zone
  274. // identifier depends on the version of Windows. So only testing that there is
  275. // a zone annotation.
  276. EXPECT_FALSE(zone_identifier.empty());
  277. }
  278. // URLs longer than INTERNET_MAX_URL_LENGTH are known to break URLMon. Such a
  279. // URL, when used as a source URL shouldn't break QuarantineFile() which should
  280. // mark the file as being from the internet zone as a safe fallback.
  281. TEST_F(QuarantineWinTest, SuperLongURL) {
  282. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  283. ASSERT_TRUE(CreateFile(test_file));
  284. std::string source_url("http://example.com/");
  285. source_url.append(INTERNET_MAX_URL_LENGTH * 2, 'a');
  286. QuarantineFile(
  287. test_file, GURL(source_url), GURL(), std::string(),
  288. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  289. base::RunLoop().RunUntilIdle();
  290. std::string zone_identifier;
  291. ASSERT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  292. // The actual assigned zone could be anything and the contents of the zone
  293. // identifier depends on the version of Windows. So only testing that there is
  294. // a zone annotation.
  295. EXPECT_FALSE(zone_identifier.empty());
  296. }
  297. TEST_F(QuarantineWinTest, TrustedSite) {
  298. // Test file path and source URL.
  299. base::FilePath test_file = GetTempDir().AppendASCII("good.exe");
  300. GURL source_url = GURL(base::WideToUTF8(
  301. base::StringPrintf(L"https://%ls/folder/good.exe", GetTrustedSite())));
  302. ASSERT_TRUE(CreateFile(test_file));
  303. QuarantineFile(
  304. test_file, source_url, GURL(), kDummyClientGuid,
  305. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  306. base::RunLoop().RunUntilIdle();
  307. // No zone identifier.
  308. std::string zone_identifier;
  309. EXPECT_FALSE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  310. }
  311. TEST_F(QuarantineWinTest, RestrictedSite) {
  312. // Test file path and source URL.
  313. base::FilePath test_file = GetTempDir().AppendASCII("bad.exe");
  314. GURL source_url = GURL(base::WideToUTF8(
  315. base::StringPrintf(L"https://%ls/folder/bad.exe", GetRestrictedSite())));
  316. ASSERT_TRUE(CreateFile(test_file));
  317. // Files from a restricted site are deleted.
  318. QuarantineFile(test_file, source_url, GURL(), kDummyClientGuid,
  319. base::BindOnce(&CheckQuarantineResult,
  320. QuarantineFileResult::BLOCKED_BY_POLICY));
  321. base::RunLoop().RunUntilIdle();
  322. std::string zone_identifier;
  323. EXPECT_FALSE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  324. }
  325. TEST_F(QuarantineWinTest, TrustedSite_AlreadyQuarantined) {
  326. // Test file path and source URL.
  327. base::FilePath test_file = GetTempDir().AppendASCII("good.exe");
  328. GURL source_url = GURL(base::WideToUTF8(
  329. base::StringPrintf(L"https://%ls/folder/good.exe", GetTrustedSite())));
  330. ASSERT_TRUE(CreateFile(test_file));
  331. // Ensure the file already contains a zone identifier.
  332. ASSERT_TRUE(AddInternetZoneIdentifierDirectly(test_file));
  333. QuarantineFile(
  334. test_file, source_url, GURL(), kDummyClientGuid,
  335. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  336. base::RunLoop().RunUntilIdle();
  337. // The existing zone identifier was not removed.
  338. std::string zone_identifier;
  339. EXPECT_TRUE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  340. EXPECT_FALSE(zone_identifier.empty());
  341. }
  342. TEST_F(QuarantineWinTest, RestrictedSite_AlreadyQuarantined) {
  343. // Test file path and source URL.
  344. base::FilePath test_file = GetTempDir().AppendASCII("bad.exe");
  345. GURL source_url = GURL(base::WideToUTF8(
  346. base::StringPrintf(L"https://%ls/folder/bad.exe", GetRestrictedSite())));
  347. ASSERT_TRUE(CreateFile(test_file));
  348. // Ensure the file already contains a zone identifier.
  349. ASSERT_TRUE(AddInternetZoneIdentifierDirectly(test_file));
  350. // Files from a restricted site are deleted.
  351. QuarantineFile(test_file, source_url, GURL(), kDummyClientGuid,
  352. base::BindOnce(&CheckQuarantineResult,
  353. QuarantineFileResult::BLOCKED_BY_POLICY));
  354. base::RunLoop().RunUntilIdle();
  355. std::string zone_identifier;
  356. EXPECT_FALSE(GetZoneIdentifierStreamContents(test_file, &zone_identifier));
  357. }
  358. TEST_F(QuarantineWinTest, MetaData_ApplyMOTW_Directly) {
  359. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  360. ASSERT_TRUE(CreateFile(test_file));
  361. GURL host_url = GURL(base::WideToUTF8(base::StringPrintf(
  362. L"https://user:pass@%ls/folder/foo.exe?x#y", GetInternetSite())));
  363. GURL host_url_clean = GURL(base::WideToUTF8(base::StringPrintf(
  364. L"https://%ls/folder/foo.exe?x#y", GetInternetSite())));
  365. GURL referrer_url = GURL(base::WideToUTF8(base::StringPrintf(
  366. L"https://user:pass@%ls/folder/index?x#y", GetInternetSite())));
  367. GURL referrer_url_clean = GURL(base::WideToUTF8(
  368. base::StringPrintf(L"https://%ls/folder/index?x#y", GetInternetSite())));
  369. // An invalid GUID will cause QuarantineFile() to apply the MOTW directly.
  370. QuarantineFile(
  371. test_file, host_url, referrer_url, std::string(),
  372. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  373. base::RunLoop().RunUntilIdle();
  374. EXPECT_TRUE(IsFileQuarantined(test_file, host_url_clean, referrer_url_clean));
  375. }
  376. TEST_F(QuarantineWinTest, MetaData_InvokeAS) {
  377. base::FilePath test_file = GetTempDir().AppendASCII("foo.exe");
  378. ASSERT_TRUE(CreateFile(test_file));
  379. GURL host_url = GURL(base::WideToUTF8(base::StringPrintf(
  380. L"https://%ls/folder/foo.exe?x#y", GetInternetSite())));
  381. GURL host_url_clean = GURL(base::WideToUTF8(base::StringPrintf(
  382. L"https://%ls/folder/foo.exe?x#y", GetInternetSite())));
  383. GURL referrer_url = GURL(base::WideToUTF8(base::StringPrintf(
  384. L"https://user:pass@%ls/folder/index?x#y", GetInternetSite())));
  385. GURL referrer_url_clean = GURL(base::WideToUTF8(
  386. base::StringPrintf(L"https://%ls/folder/index?x#y", GetInternetSite())));
  387. QuarantineFile(
  388. test_file, host_url, referrer_url, kDummyClientGuid,
  389. base::BindOnce(&CheckQuarantineResult, QuarantineFileResult::OK));
  390. base::RunLoop().RunUntilIdle();
  391. EXPECT_TRUE(IsFileQuarantined(test_file, host_url_clean, referrer_url_clean));
  392. }
  393. } // namespace quarantine