summaryrefslogtreecommitdiffstats
path: root/xpcom/io/FilePreferences.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xpcom/io/FilePreferences.cpp373
1 files changed, 373 insertions, 0 deletions
diff --git a/xpcom/io/FilePreferences.cpp b/xpcom/io/FilePreferences.cpp
new file mode 100644
index 0000000000..1d96c72810
--- /dev/null
+++ b/xpcom/io/FilePreferences.cpp
@@ -0,0 +1,373 @@
+/* -*- 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<nsString> 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<bool, Relaxed> sForbiddenPathsEmptyQuickCheck{false};
+
+typedef nsTArray<nsTString<char_path_t>> Paths;
+static StaticAutoPtr<Paths> sForbiddenPaths;
+
+static Paths& ForbiddenPaths() {
+ sMutex.AssertCurrentThreadOwns();
+ if (!sForbiddenPaths) {
+ sForbiddenPaths = new nsTArray<nsTString<char_path_t>>();
+ ClearOnShutdown(&sForbiddenPaths);
+ }
+ return *sForbiddenPaths;
+}
+
+static void AllowUNCDirectory(char const* directory) {
+ nsCOMPtr<nsIFile> 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<char_path_t> 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<char_path_t> p(forbidden);
+ while (!p.CheckEOF()) {
+ nsTString<char_path_t> path;
+ Unused << p.ReadUntil(TTokenizer<char_path_t>::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 <typename TChar>
+class TNormalizer : public TTokenizer<TChar> {
+ typedef TTokenizer<TChar> base;
+
+ public:
+ typedef typename base::Token Token;
+
+ TNormalizer(const nsTSubstring<TChar>& aFilePath, const Token& aSeparator)
+ : TTokenizer<TChar>(aFilePath), mSeparator(aSeparator) {}
+
+ bool Get(nsTSubstring<TChar>& 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<TChar> 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<TChar>::const_char_iterator cursor = base::mCursor;
+ if (base::CheckChar('.') && base::CheckChar('.') && CheckSeparator()) {
+ return true;
+ }
+
+ base::mCursor = cursor;
+ return false;
+ }
+
+ bool CheckCurrentDir() {
+ typename nsTString<TChar>::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<nsTDependentSubstring<TChar>> 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<char16_t> 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<char_path_t>& aFilePath) {
+ typedef TNormalizer<char_path_t> 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<char_path_t> 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<char16_t> Normalizer;
+ Normalizer normalizer(aPath, Normalizer::Token::Char('\\'));
+ return normalizer.Get(aNormalized);
+}
+
+} // namespace FilePreferences
+} // namespace mozilla