File: incognito_profile_containment_browsertest.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (332 lines) | stat: -rw-r--r-- 12,186 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <algorithm>

#include "base/containers/contains.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/common/content_paths.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace {

// List of file or directory prefixes that are known to be modified during an
// Incognito session.
// For ChromeOS, a copy of all members of |kAllowListPrefixesForAllPlatforms|
// that start with "/Default" is added to the allow list, replacing "/Default"
// with "/test-user".
// TODO(http://crbug.com/1234755): Add audit comment (or fix the issue) for all
// paths that do not have a comment.
const char* kAllowListPrefixesForAllPlatforms[] = {
    "/Default/data_reduction_proxy_leveldb",
    "/Default/Extension State",
    "/Default/GCM Store/",
    "/Default/Network Action Predictor",
    "/Default/Preferences",
    "/Default/PreferredApps",
    "/Default/Reporting and NEL",
    "/Default/shared_proto_db",
    "/Default/Trust Tokens",
    "/Default/Shortcuts",
    "/GrShaderCache/GPUCache",
    "/Local State"};
#if BUILDFLAG(IS_MAC)
const char* kAllowListPrefixesForPlatform[] = {"/Default/Visited Links"};
#elif BUILDFLAG(IS_WIN)
const char* kAllowListPrefixesForPlatform[] = {
    "/Default/databases-off-the-record",
    "/Default/heavy_ad_intervention_opt_out.db", "/Default/Top Sites",
    "/GrShaderCache/old_GPUCache",

    // This file only contains the path to the latest executable of Chrome,
    // therefore it's safe to be written in Incognito.
    "/Last Browser"};
#elif BUILDFLAG(IS_CHROMEOS)
const char* kAllowListPrefixesForPlatform[] = {
    "/Default/Local Storage/leveldb/CURRENT",
    "/Default/Site Characteristics Database", "/Default/Sync Data/LevelDB",
    "/test-user/.variations-list.txt"};
#elif BUILDFLAG(IS_LINUX)
const char* kAllowListPrefixesForPlatform[] = {"/Default/Web Data"};
#else
const char* kAllowListPrefixesForPlatform[] = {};
#endif

// List of directory prefixes that are known to be added as an empty directory
// during an Incognito session.
// TODO(http://crbug.com/1234755): Add audit comment (or fix the issue) for all
// paths that do not have a comment.
const char* kAllowListEmptyDirectoryPrefixesForAllPlatforms[] = {
    "/Default/AutofillStrikeDatabase",
    "/Default/Download Service",
    "/Default/Feature Engagement Tracker",
    "/Default/GCM Store/Encryption",
    "/Default/optimization_guide_hint_cache_store",
    "/Default/optimization_guide_model_and_features_store",
    "/Default/shared_proto_db/metadata",
    "/test-user"};

// Structure that keeps data about a snapshotted file.
struct FileData {
  base::FilePath full_path;
  base::Time last_modified_time;
  int64_t size = 0;
  bool file_hash_is_valid = false;
  uint32_t file_hash = 0;
};

struct Snapshot {
  std::unordered_map<std::string, FileData> files;
  std::unordered_set<std::string> directories;
};

bool ComputeFileHash(const base::FilePath& file_path, uint32_t* hash_code) {
  std::string content;
  base::ScopedAllowBlockingForTesting allow_blocking;

  if (!base::ReadFileToString(file_path, &content))
    return false;
  *hash_code = base::Hash(content);
  return true;
}

void GetUserDirectorySnapshot(Snapshot& snapshot, bool compute_file_hashes) {
  base::FilePath user_data_dir =
      g_browser_process->profile_manager()->user_data_dir();
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::FileEnumerator enumerator(
      user_data_dir, true /* recursive */,
      base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);

  for (base::FilePath path = enumerator.Next(); !path.empty();
       path = enumerator.Next()) {
    // Remove |user_data_dir| part from path.
    std::string reduced_path =
        path.NormalizePathSeparatorsTo('/').AsUTF8Unsafe().substr(
            user_data_dir.AsUTF8Unsafe().length());

    if (enumerator.GetInfo().IsDirectory()) {
      snapshot.directories.insert(reduced_path);
    } else {
      FileData fd;
      fd.size = enumerator.GetInfo().GetSize();
      fd.last_modified_time = enumerator.GetInfo().GetLastModifiedTime();
      fd.file_hash_is_valid =
          compute_file_hashes ? ComputeFileHash(path, &fd.file_hash) : false;
      fd.full_path = path;
      snapshot.files[reduced_path] = fd;
    }
  }
  return;
}

bool IsFileModified(FileData& before, FileData& after) {
  // TODO(http://crbug.com/1234755): Also consider auditing files that are
  // touched or are unreadable.
  // If it was readable before, and is readable now, compare hash codes.
  if (before.file_hash_is_valid) {
    uint32_t hash_code;
    if (!ComputeFileHash(after.full_path, &hash_code))
      return false;

    return hash_code != before.file_hash;
  }

  return false;
}

bool AreDirectoriesModified(Snapshot& snapshot_before,
                            Snapshot& snapshot_after,
                            std::set<std::string>& allow_list) {
  bool modified = false;

  // Check for new directories.
  for (const std::string& directory : snapshot_after.directories) {
    if (!base::Contains(snapshot_before.directories, directory)) {
      // If a file/prefix in this directory is allowlisted, ignore directory
      // addition.
      if (std::ranges::any_of(allow_list,
                              [&directory](const std::string& prefix) {
                                return prefix.find(directory) == 0;
                              })) {
        continue;
      }

      // If directory is specifically allow list, ignore.
      if (std::ranges::any_of(
              kAllowListEmptyDirectoryPrefixesForAllPlatforms,
              [&directory](const std::string& allow_listed_directory) {
                return directory.find(allow_listed_directory) == 0;
              })) {
        continue;
      }

      LOG(ERROR) << "New directory: " << directory;
      modified = true;
    }
  }

  return modified;
}

bool AreFilesModified(Snapshot& snapshot_before,
                      Snapshot& snapshot_after,
                      std::set<std::string>& allow_list) {
  bool modified = false;

  // TODO(http://crbug.com/1234755): Consider deleted files as well. Currently
  // we only look for added and modified files, but file deletion is also
  // modifying disk and is best to be prevented.
  for (auto& fd : snapshot_after.files) {
    auto before = snapshot_before.files.find(fd.first);
    bool is_new = (before == snapshot_before.files.end());
    if (is_new ||
        fd.second.last_modified_time != before->second.last_modified_time) {
      // Ignore allow-listed paths.
      if (std::ranges::any_of(allow_list, [&fd](const std::string& prefix) {
            return fd.first.find(prefix) == 0;
          })) {
        continue;
      }

      // If an empty file is added or modified, ignore for now.
      // TODO(http://crbug.com/1234755): Consider newly added empty files.
      if (!fd.second.size)
        continue;

      // If data content is not changed, it can be ignored.
      if (!is_new && !IsFileModified(before->second, fd.second))
        continue;

      modified = true;

      LOG(ERROR) << (is_new ? "New" : "Modified") << " File " << fd.first
                 << std::string(" - Size: ") +
                        base::NumberToString(fd.second.size);
    }
  }
  return modified;
}

}  // namespace

class IncognitoProfileContainmentBrowserTest : public InProcessBrowserTest {
 public:
  IncognitoProfileContainmentBrowserTest()
      : allow_list_(std::begin(kAllowListPrefixesForAllPlatforms),
                    std::end(kAllowListPrefixesForAllPlatforms)) {
#if BUILDFLAG(IS_CHROMEOS)
    // These prefixes are allowed twice, once under "Default" and once under
    // "test-user".
    std::set<std::string> test_folder;
    const int offset = strlen("/Default");
    for (std::string prefix : allow_list_) {
      if (prefix.find("/Default/") == 0) {
        test_folder.insert(std::string("/test-user") + prefix.substr(offset));
      }
    }
    allow_list_.insert(test_folder.begin(), test_folder.end());
#endif

    for (const char* platform_prefix : kAllowListPrefixesForPlatform) {
      allow_list_.emplace(platform_prefix);
    }
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    base::FilePath path;
    base::PathService::Get(content::DIR_TEST_DATA, &path);
    embedded_test_server()->ServeFilesFromDirectory(path);
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    InProcessBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kIncognito);
  }

 protected:
  std::set<std::string> allow_list_;
};

// Open a page in a separate session to ensure all files that are created
// because of the regular profile start up are already created.
IN_PROC_BROWSER_TEST_F(IncognitoProfileContainmentBrowserTest,
                       PRE_StoringDataDoesNotModifyProfileFolder) {
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/empty.html")));
}

// Test that calling several data storage APIs does not modify regular profile
// directory.
// If you are storing from a "regular" (non off-the-record) profile and your CL
// breaks this test, please first check if it is intended to change profile
// state even if user did not explicitly open the browser in regular mode and if
// so, please add the file to the allow_list at the top and file a bug to follow
// up.
// TODO(crbug.com/40809832): Flakes on Win 7.
IN_PROC_BROWSER_TEST_F(IncognitoProfileContainmentBrowserTest,
                       DISABLED_StoringDataDoesNotModifyProfileFolder) {
  // Take a snapshot of regular profile.
  Snapshot before_incognito;
  GetUserDirectorySnapshot(before_incognito, /*compute_file_hashes=*/true);

  // Run an Incognito session.
  Browser* browser = chrome::FindLastActive();
  EXPECT_TRUE(browser->profile()->IsOffTheRecord());
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser,
      embedded_test_server()->GetURL("/browsing_data/site_data.html")));

  const std::vector<std::string> kStorageTypes{
      "CacheStorage", "Cookie",        "FileSystem",    "IndexedDb",
      "LocalStorage", "ServiceWorker", "SessionCookie", "WebSql"};

  for (const std::string& type : kStorageTypes) {
    ASSERT_TRUE(
        content::EvalJs(browser->tab_strip_model()->GetActiveWebContents(),
                        "set" + type + "()")
            .ExtractBool())
        << "Couldn't create data for: " << type;
  }

  CloseBrowserSynchronously(browser);

  // Take another snapshot of regular profile and ensure it is not changed.
  // Do not compute file content hashes for faster processing. They would be
  // computed only if needed.
  Snapshot after_incognito;
  GetUserDirectorySnapshot(after_incognito, /*compute_file_hashes=*/false);
  EXPECT_FALSE(
      AreFilesModified(before_incognito, after_incognito, allow_list_));

  // TODO(http://crbug.com/1234755): Change to EXPECT_FALSE.
  if (AreDirectoriesModified(before_incognito, after_incognito, allow_list_)) {
    LOG(ERROR) << "Empty directories added.";
  }
}

// TODO(http://crbug.com/1234755): Add more complex naviagtions, triggering
// different APIs in "browsing_data/site_data.html" and more.