/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FilePreferences.h" #include "mozilla/Atomics.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPtr.h" #include "mozilla/TextUtils.h" #include "mozilla/Tokenizer.h" #include "nsAppDirectoryServiceDefs.h" #include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsString.h" namespace mozilla { namespace FilePreferences { static StaticMutex sMutex; static bool sBlockUNCPaths = false; typedef nsTArray WinPaths; static WinPaths& PathAllowlist() MOZ_REQUIRES(sMutex) { sMutex.AssertCurrentThreadOwns(); static WinPaths sPaths MOZ_GUARDED_BY(sMutex); return sPaths; } #ifdef XP_WIN const auto kDevicePathSpecifier = u"\\\\?\\"_ns; typedef char16_t char_path_t; #else typedef char char_path_t; #endif // Initially false to make concurrent consumers acquire the lock and sync. // The plain bool is synchronized with sMutex, the atomic one is for a quick // check w/o the need to acquire the lock on the hot path. static bool sForbiddenPathsEmpty = false; static Atomic sForbiddenPathsEmptyQuickCheck{false}; typedef nsTArray> Paths; static StaticAutoPtr sForbiddenPaths; static Paths& ForbiddenPaths() { sMutex.AssertCurrentThreadOwns(); if (!sForbiddenPaths) { sForbiddenPaths = new nsTArray>(); ClearOnShutdown(&sForbiddenPaths); } return *sForbiddenPaths; } static void AllowUNCDirectory(char const* directory) { nsCOMPtr file; NS_GetSpecialDirectory(directory, getter_AddRefs(file)); if (!file) { return; } nsString path; if (NS_FAILED(file->GetTarget(path))) { return; } // The allowlist makes sense only for UNC paths, because this code is used // to block only UNC paths, hence, no need to add non-UNC directories here // as those would never pass the check. if (!StringBeginsWith(path, u"\\\\"_ns)) { return; } StaticMutexAutoLock lock(sMutex); if (!PathAllowlist().Contains(path)) { PathAllowlist().AppendElement(path); } } void InitPrefs() { sBlockUNCPaths = Preferences::GetBool("network.file.disable_unc_paths", false); nsTAutoString forbidden; #ifdef XP_WIN Preferences::GetString("network.file.path_blacklist", forbidden); #else Preferences::GetCString("network.file.path_blacklist", forbidden); #endif StaticMutexAutoLock lock(sMutex); if (forbidden.IsEmpty()) { sForbiddenPathsEmptyQuickCheck = (sForbiddenPathsEmpty = true); return; } ForbiddenPaths().Clear(); TTokenizer p(forbidden); while (!p.CheckEOF()) { nsTString path; Unused << p.ReadUntil(TTokenizer::Token::Char(','), path); path.Trim(" "); if (!path.IsEmpty()) { ForbiddenPaths().AppendElement(path); } Unused << p.CheckChar(','); } sForbiddenPathsEmptyQuickCheck = (sForbiddenPathsEmpty = ForbiddenPaths().Length() == 0); } void InitDirectoriesAllowlist() { // NS_GRE_DIR is the installation path where the binary resides. AllowUNCDirectory(NS_GRE_DIR); // NS_APP_USER_PROFILE_50_DIR and NS_APP_USER_PROFILE_LOCAL_50_DIR are the two // parts of the profile we store permanent and local-specific data. AllowUNCDirectory(NS_APP_USER_PROFILE_50_DIR); AllowUNCDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR); } namespace { // anon template class TNormalizer : public TTokenizer { typedef TTokenizer base; public: typedef typename base::Token Token; TNormalizer(const nsTSubstring& aFilePath, const Token& aSeparator) : TTokenizer(aFilePath), mSeparator(aSeparator) {} bool Get(nsTSubstring& aNormalizedFilePath) { aNormalizedFilePath.Truncate(); // Windows UNC paths begin with double separator (\\) // Linux paths begin with just one separator (/) // If we want to use the normalizer for regular windows paths this code // will need to be updated. #ifdef XP_WIN if (base::Check(mSeparator)) { aNormalizedFilePath.Append(mSeparator.AsChar()); } #endif if (base::Check(mSeparator)) { aNormalizedFilePath.Append(mSeparator.AsChar()); } while (base::HasInput()) { if (!ConsumeName()) { return false; } } for (auto const& name : mStack) { aNormalizedFilePath.Append(name); } return true; } private: bool ConsumeName() { if (base::CheckEOF()) { return true; } if (CheckCurrentDir()) { return true; } if (CheckParentDir()) { if (!mStack.Length()) { // This means there are more \.. than valid names return false; } mStack.RemoveLastElement(); return true; } nsTDependentSubstring name; if (base::ReadUntil(mSeparator, name, base::INCLUDE_LAST) && name.Length() == 1) { // this means an empty name (a lone slash), which is illegal return false; } mStack.AppendElement(name); return true; } bool CheckParentDir() { typename nsTString::const_char_iterator cursor = base::mCursor; if (base::CheckChar('.') && base::CheckChar('.') && CheckSeparator()) { return true; } base::mCursor = cursor; return false; } bool CheckCurrentDir() { typename nsTString::const_char_iterator cursor = base::mCursor; if (base::CheckChar('.') && CheckSeparator()) { return true; } base::mCursor = cursor; return false; } bool CheckSeparator() { return base::Check(mSeparator) || base::CheckEOF(); } Token const mSeparator; nsTArray> mStack; }; #ifdef XP_WIN bool IsDOSDevicePathWithDrive(const nsAString& aFilePath) { if (!StringBeginsWith(aFilePath, kDevicePathSpecifier)) { return false; } const auto pathNoPrefix = nsDependentSubstring(aFilePath, kDevicePathSpecifier.Length()); // After the device path specifier, the rest of file path can be: // - starts with the volume or drive. e.g. \\?\C:\... // - UNCs. e.g. \\?\UNC\Server\Share\Test\Foo.txt // - device UNCs. e.g. \\?\server1\e:\utilities\\filecomparer\... // The first case should not be blocked by IsBlockedUNCPath. if (!StartsWithDiskDesignatorAndBackslash(pathNoPrefix)) { return false; } return true; } #endif } // namespace bool IsBlockedUNCPath(const nsAString& aFilePath) { typedef TNormalizer Normalizer; if (!sBlockUNCPaths) { return false; } if (!StringBeginsWith(aFilePath, u"\\\\"_ns)) { return false; } #ifdef XP_WIN // ToDo: We don't need to check this once we can check if there is a valid // server or host name that is prefaced by "\\". // https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats if (IsDOSDevicePathWithDrive(aFilePath)) { return false; } #endif nsAutoString normalized; if (!Normalizer(aFilePath, Normalizer::Token::Char('\\')).Get(normalized)) { // Broken paths are considered invalid and thus inaccessible return true; } StaticMutexAutoLock lock(sMutex); for (const auto& allowedPrefix : PathAllowlist()) { if (StringBeginsWith(normalized, allowedPrefix)) { if (normalized.Length() == allowedPrefix.Length()) { return false; } if (normalized[allowedPrefix.Length()] == L'\\') { return false; } // When we are here, the path has a form "\\path\prefixevil" // while we have an allowed prefix of "\\path\prefix". // Note that we don't want to add a slash to the end of a prefix // so that opening the directory (no slash at the end) still works. break; } } return true; } #ifdef XP_WIN const char kPathSeparator = '\\'; #else const char kPathSeparator = '/'; #endif bool IsAllowedPath(const nsTSubstring& aFilePath) { typedef TNormalizer Normalizer; // An atomic quick check out of the lock, because this is mostly `true`. if (sForbiddenPathsEmptyQuickCheck) { return true; } StaticMutexAutoLock lock(sMutex); if (sForbiddenPathsEmpty) { return true; } // If sForbidden has been cleared at shutdown, we must avoid calling // ForbiddenPaths() again, as that will recreate the array and we will leak. if (!sForbiddenPaths) { return true; } nsTAutoString normalized; if (!Normalizer(aFilePath, Normalizer::Token::Char(kPathSeparator)) .Get(normalized)) { // Broken paths are considered invalid and thus inaccessible return false; } for (const auto& prefix : ForbiddenPaths()) { if (StringBeginsWith(normalized, prefix)) { if (normalized.Length() > prefix.Length() && normalized[prefix.Length()] != kPathSeparator) { continue; } return false; } } return true; } #ifdef XP_WIN bool StartsWithDiskDesignatorAndBackslash(const nsAString& aAbsolutePath) { // aAbsolutePath can only be (in regular expression): // UNC path: ^\\\\.* // A single backslash: ^\\.* // A disk designator with a backslash: ^[A-Za-z]:\\.* return aAbsolutePath.Length() >= 3 && IsAsciiAlpha(aAbsolutePath.CharAt(0)) && aAbsolutePath.CharAt(1) == L':' && aAbsolutePath.CharAt(2) == kPathSeparator; } #endif void testing::SetBlockUNCPaths(bool aBlock) { sBlockUNCPaths = aBlock; } void testing::AddDirectoryToAllowlist(nsAString const& aPath) { StaticMutexAutoLock lock(sMutex); PathAllowlist().AppendElement(aPath); } bool testing::NormalizePath(nsAString const& aPath, nsAString& aNormalized) { typedef TNormalizer Normalizer; Normalizer normalizer(aPath, Normalizer::Token::Char('\\')); return normalizer.Get(aNormalized); } } // namespace FilePreferences } // namespace mozilla