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"
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
);
37 const auto kDevicePathSpecifier
= u
"\\\\?\\"_ns
;
39 typedef char16_t char_path_t
;
41 typedef char char_path_t
;
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
));
70 if (NS_FAILED(file
->GetTarget(path
))) {
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
)) {
81 StaticMutexAutoLock
lock(sMutex
);
83 if (!PathAllowlist().Contains(path
)) {
84 PathAllowlist().AppendElement(path
);
90 Preferences::GetBool("network.file.disable_unc_paths", false);
92 nsTAutoString
<char_path_t
> forbidden
;
94 Preferences::GetString("network.file.path_blacklist", forbidden
);
96 Preferences::GetCString("network.file.path_blacklist", forbidden
);
99 StaticMutexAutoLock
lock(sMutex
);
101 if (forbidden
.IsEmpty()) {
102 sForbiddenPathsEmptyQuickCheck
= (sForbiddenPathsEmpty
= true);
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
);
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
);
133 template <typename TChar
>
134 class TNormalizer
: public TTokenizer
<TChar
> {
135 typedef TTokenizer
<TChar
> base
;
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.
151 if (base::Check(mSeparator
)) {
152 aNormalizedFilePath
.Append(mSeparator
.AsChar());
156 if (base::Check(mSeparator
)) {
157 aNormalizedFilePath
.Append(mSeparator
.AsChar());
160 while (base::HasInput()) {
161 if (!ConsumeName()) {
166 for (auto const& name
: mStack
) {
167 aNormalizedFilePath
.Append(name
);
175 if (base::CheckEOF()) {
179 if (CheckCurrentDir()) {
183 if (CheckParentDir()) {
184 if (!mStack
.Length()) {
185 // This means there are more \.. than valid names
189 mStack
.RemoveLastElement();
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
199 mStack
.AppendElement(name
);
204 bool CheckParentDir() {
205 typename nsTString
<TChar
>::const_char_iterator cursor
= base::mCursor
;
206 if (base::CheckChar('.') && base::CheckChar('.') && CheckSeparator()) {
210 base::mCursor
= cursor
;
214 bool CheckCurrentDir() {
215 typename nsTString
<TChar
>::const_char_iterator cursor
= base::mCursor
;
216 if (base::CheckChar('.') && CheckSeparator()) {
220 base::mCursor
= cursor
;
224 bool CheckSeparator() { return base::Check(mSeparator
) || base::CheckEOF(); }
226 Token
const mSeparator
;
227 nsTArray
<nsTDependentSubstring
<TChar
>> mStack
;
231 bool IsDOSDevicePathWithDrive(const nsAString
& aFilePath
) {
232 if (!StringBeginsWith(aFilePath
, kDevicePathSpecifier
)) {
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
)) {
254 bool IsBlockedUNCPath(const nsAString
& aFilePath
) {
255 typedef TNormalizer
<char16_t
> Normalizer
;
256 if (!sBlockUNCPaths
) {
260 if (!StringBeginsWith(aFilePath
, u
"\\\\"_ns
)) {
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
)) {
273 nsAutoString normalized
;
274 if (!Normalizer(aFilePath
, Normalizer::Token::Char('\\')).Get(normalized
)) {
275 // Broken paths are considered invalid and thus inaccessible
279 StaticMutexAutoLock
lock(sMutex
);
281 for (const auto& allowedPrefix
: PathAllowlist()) {
282 if (StringBeginsWith(normalized
, allowedPrefix
)) {
283 if (normalized
.Length() == allowedPrefix
.Length()) {
286 if (normalized
[allowedPrefix
.Length()] == L
'\\') {
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.
302 const char kPathSeparator
= '\\';
304 const char kPathSeparator
= '/';
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
) {
315 StaticMutexAutoLock
lock(sMutex
);
317 if (sForbiddenPathsEmpty
) {
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
) {
327 nsTAutoString
<char_path_t
> normalized
;
328 if (!Normalizer(aFilePath
, Normalizer::Token::Char(kPathSeparator
))
330 // Broken paths are considered invalid and thus inaccessible
334 for (const auto& prefix
: ForbiddenPaths()) {
335 if (StringBeginsWith(normalized
, prefix
)) {
336 if (normalized
.Length() > prefix
.Length() &&
337 normalized
[prefix
.Length()] != kPathSeparator
) {
348 bool StartsWithDiskDesignatorAndBackslash(const nsAString
& aAbsolutePath
) {
349 // aAbsolutePath can only be (in regular expression):
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
;
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