Merge autoland to mozilla-central. a=merge
[gecko.git] / xpcom / io / FilePreferences.cpp
blob1d96c7281016e7a982fa6abe93f382b85773bb1a
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "FilePreferences.h"
9 #include "mozilla/Atomics.h"
10 #include "mozilla/ClearOnShutdown.h"
11 #include "mozilla/Preferences.h"
12 #include "mozilla/StaticMutex.h"
13 #include "mozilla/StaticPtr.h"
14 #include "mozilla/TextUtils.h"
15 #include "mozilla/Tokenizer.h"
16 #include "nsAppDirectoryServiceDefs.h"
17 #include "nsDirectoryServiceDefs.h"
18 #include "nsDirectoryServiceUtils.h"
19 #include "nsString.h"
21 namespace mozilla {
22 namespace FilePreferences {
24 static StaticMutex sMutex;
26 static bool sBlockUNCPaths = false;
27 typedef nsTArray<nsString> WinPaths;
29 static WinPaths& PathAllowlist() MOZ_REQUIRES(sMutex) {
30 sMutex.AssertCurrentThreadOwns();
32 static WinPaths sPaths MOZ_GUARDED_BY(sMutex);
33 return sPaths;
36 #ifdef XP_WIN
37 const auto kDevicePathSpecifier = u"\\\\?\\"_ns;
39 typedef char16_t char_path_t;
40 #else
41 typedef char char_path_t;
42 #endif
44 // Initially false to make concurrent consumers acquire the lock and sync.
45 // The plain bool is synchronized with sMutex, the atomic one is for a quick
46 // check w/o the need to acquire the lock on the hot path.
47 static bool sForbiddenPathsEmpty = false;
48 static Atomic<bool, Relaxed> sForbiddenPathsEmptyQuickCheck{false};
50 typedef nsTArray<nsTString<char_path_t>> Paths;
51 static StaticAutoPtr<Paths> sForbiddenPaths;
53 static Paths& ForbiddenPaths() {
54 sMutex.AssertCurrentThreadOwns();
55 if (!sForbiddenPaths) {
56 sForbiddenPaths = new nsTArray<nsTString<char_path_t>>();
57 ClearOnShutdown(&sForbiddenPaths);
59 return *sForbiddenPaths;
62 static void AllowUNCDirectory(char const* directory) {
63 nsCOMPtr<nsIFile> file;
64 NS_GetSpecialDirectory(directory, getter_AddRefs(file));
65 if (!file) {
66 return;
69 nsString path;
70 if (NS_FAILED(file->GetTarget(path))) {
71 return;
74 // The allowlist makes sense only for UNC paths, because this code is used
75 // to block only UNC paths, hence, no need to add non-UNC directories here
76 // as those would never pass the check.
77 if (!StringBeginsWith(path, u"\\\\"_ns)) {
78 return;
81 StaticMutexAutoLock lock(sMutex);
83 if (!PathAllowlist().Contains(path)) {
84 PathAllowlist().AppendElement(path);
88 void InitPrefs() {
89 sBlockUNCPaths =
90 Preferences::GetBool("network.file.disable_unc_paths", false);
92 nsTAutoString<char_path_t> forbidden;
93 #ifdef XP_WIN
94 Preferences::GetString("network.file.path_blacklist", forbidden);
95 #else
96 Preferences::GetCString("network.file.path_blacklist", forbidden);
97 #endif
99 StaticMutexAutoLock lock(sMutex);
101 if (forbidden.IsEmpty()) {
102 sForbiddenPathsEmptyQuickCheck = (sForbiddenPathsEmpty = true);
103 return;
106 ForbiddenPaths().Clear();
107 TTokenizer<char_path_t> p(forbidden);
108 while (!p.CheckEOF()) {
109 nsTString<char_path_t> path;
110 Unused << p.ReadUntil(TTokenizer<char_path_t>::Token::Char(','), path);
111 path.Trim(" ");
112 if (!path.IsEmpty()) {
113 ForbiddenPaths().AppendElement(path);
115 Unused << p.CheckChar(',');
118 sForbiddenPathsEmptyQuickCheck =
119 (sForbiddenPathsEmpty = ForbiddenPaths().Length() == 0);
122 void InitDirectoriesAllowlist() {
123 // NS_GRE_DIR is the installation path where the binary resides.
124 AllowUNCDirectory(NS_GRE_DIR);
125 // NS_APP_USER_PROFILE_50_DIR and NS_APP_USER_PROFILE_LOCAL_50_DIR are the two
126 // parts of the profile we store permanent and local-specific data.
127 AllowUNCDirectory(NS_APP_USER_PROFILE_50_DIR);
128 AllowUNCDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR);
131 namespace { // anon
133 template <typename TChar>
134 class TNormalizer : public TTokenizer<TChar> {
135 typedef TTokenizer<TChar> base;
137 public:
138 typedef typename base::Token Token;
140 TNormalizer(const nsTSubstring<TChar>& aFilePath, const Token& aSeparator)
141 : TTokenizer<TChar>(aFilePath), mSeparator(aSeparator) {}
143 bool Get(nsTSubstring<TChar>& aNormalizedFilePath) {
144 aNormalizedFilePath.Truncate();
146 // Windows UNC paths begin with double separator (\\)
147 // Linux paths begin with just one separator (/)
148 // If we want to use the normalizer for regular windows paths this code
149 // will need to be updated.
150 #ifdef XP_WIN
151 if (base::Check(mSeparator)) {
152 aNormalizedFilePath.Append(mSeparator.AsChar());
154 #endif
156 if (base::Check(mSeparator)) {
157 aNormalizedFilePath.Append(mSeparator.AsChar());
160 while (base::HasInput()) {
161 if (!ConsumeName()) {
162 return false;
166 for (auto const& name : mStack) {
167 aNormalizedFilePath.Append(name);
170 return true;
173 private:
174 bool ConsumeName() {
175 if (base::CheckEOF()) {
176 return true;
179 if (CheckCurrentDir()) {
180 return true;
183 if (CheckParentDir()) {
184 if (!mStack.Length()) {
185 // This means there are more \.. than valid names
186 return false;
189 mStack.RemoveLastElement();
190 return true;
193 nsTDependentSubstring<TChar> name;
194 if (base::ReadUntil(mSeparator, name, base::INCLUDE_LAST) &&
195 name.Length() == 1) {
196 // this means an empty name (a lone slash), which is illegal
197 return false;
199 mStack.AppendElement(name);
201 return true;
204 bool CheckParentDir() {
205 typename nsTString<TChar>::const_char_iterator cursor = base::mCursor;
206 if (base::CheckChar('.') && base::CheckChar('.') && CheckSeparator()) {
207 return true;
210 base::mCursor = cursor;
211 return false;
214 bool CheckCurrentDir() {
215 typename nsTString<TChar>::const_char_iterator cursor = base::mCursor;
216 if (base::CheckChar('.') && CheckSeparator()) {
217 return true;
220 base::mCursor = cursor;
221 return false;
224 bool CheckSeparator() { return base::Check(mSeparator) || base::CheckEOF(); }
226 Token const mSeparator;
227 nsTArray<nsTDependentSubstring<TChar>> mStack;
230 #ifdef XP_WIN
231 bool IsDOSDevicePathWithDrive(const nsAString& aFilePath) {
232 if (!StringBeginsWith(aFilePath, kDevicePathSpecifier)) {
233 return false;
236 const auto pathNoPrefix =
237 nsDependentSubstring(aFilePath, kDevicePathSpecifier.Length());
239 // After the device path specifier, the rest of file path can be:
240 // - starts with the volume or drive. e.g. \\?\C:\...
241 // - UNCs. e.g. \\?\UNC\Server\Share\Test\Foo.txt
242 // - device UNCs. e.g. \\?\server1\e:\utilities\\filecomparer\...
243 // The first case should not be blocked by IsBlockedUNCPath.
244 if (!StartsWithDiskDesignatorAndBackslash(pathNoPrefix)) {
245 return false;
248 return true;
250 #endif
252 } // namespace
254 bool IsBlockedUNCPath(const nsAString& aFilePath) {
255 typedef TNormalizer<char16_t> Normalizer;
256 if (!sBlockUNCPaths) {
257 return false;
260 if (!StringBeginsWith(aFilePath, u"\\\\"_ns)) {
261 return false;
264 #ifdef XP_WIN
265 // ToDo: We don't need to check this once we can check if there is a valid
266 // server or host name that is prefaced by "\\".
267 // https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
268 if (IsDOSDevicePathWithDrive(aFilePath)) {
269 return false;
271 #endif
273 nsAutoString normalized;
274 if (!Normalizer(aFilePath, Normalizer::Token::Char('\\')).Get(normalized)) {
275 // Broken paths are considered invalid and thus inaccessible
276 return true;
279 StaticMutexAutoLock lock(sMutex);
281 for (const auto& allowedPrefix : PathAllowlist()) {
282 if (StringBeginsWith(normalized, allowedPrefix)) {
283 if (normalized.Length() == allowedPrefix.Length()) {
284 return false;
286 if (normalized[allowedPrefix.Length()] == L'\\') {
287 return false;
290 // When we are here, the path has a form "\\path\prefixevil"
291 // while we have an allowed prefix of "\\path\prefix".
292 // Note that we don't want to add a slash to the end of a prefix
293 // so that opening the directory (no slash at the end) still works.
294 break;
298 return true;
301 #ifdef XP_WIN
302 const char kPathSeparator = '\\';
303 #else
304 const char kPathSeparator = '/';
305 #endif
307 bool IsAllowedPath(const nsTSubstring<char_path_t>& aFilePath) {
308 typedef TNormalizer<char_path_t> Normalizer;
310 // An atomic quick check out of the lock, because this is mostly `true`.
311 if (sForbiddenPathsEmptyQuickCheck) {
312 return true;
315 StaticMutexAutoLock lock(sMutex);
317 if (sForbiddenPathsEmpty) {
318 return true;
321 // If sForbidden has been cleared at shutdown, we must avoid calling
322 // ForbiddenPaths() again, as that will recreate the array and we will leak.
323 if (!sForbiddenPaths) {
324 return true;
327 nsTAutoString<char_path_t> normalized;
328 if (!Normalizer(aFilePath, Normalizer::Token::Char(kPathSeparator))
329 .Get(normalized)) {
330 // Broken paths are considered invalid and thus inaccessible
331 return false;
334 for (const auto& prefix : ForbiddenPaths()) {
335 if (StringBeginsWith(normalized, prefix)) {
336 if (normalized.Length() > prefix.Length() &&
337 normalized[prefix.Length()] != kPathSeparator) {
338 continue;
340 return false;
344 return true;
347 #ifdef XP_WIN
348 bool StartsWithDiskDesignatorAndBackslash(const nsAString& aAbsolutePath) {
349 // aAbsolutePath can only be (in regular expression):
350 // UNC path: ^\\\\.*
351 // A single backslash: ^\\.*
352 // A disk designator with a backslash: ^[A-Za-z]:\\.*
353 return aAbsolutePath.Length() >= 3 && IsAsciiAlpha(aAbsolutePath.CharAt(0)) &&
354 aAbsolutePath.CharAt(1) == L':' &&
355 aAbsolutePath.CharAt(2) == kPathSeparator;
357 #endif
359 void testing::SetBlockUNCPaths(bool aBlock) { sBlockUNCPaths = aBlock; }
361 void testing::AddDirectoryToAllowlist(nsAString const& aPath) {
362 StaticMutexAutoLock lock(sMutex);
363 PathAllowlist().AppendElement(aPath);
366 bool testing::NormalizePath(nsAString const& aPath, nsAString& aNormalized) {
367 typedef TNormalizer<char16_t> Normalizer;
368 Normalizer normalizer(aPath, Normalizer::Token::Char('\\'));
369 return normalizer.Get(aNormalized);
372 } // namespace FilePreferences
373 } // namespace mozilla