Bug 1412081 - Add ability to blacklist file paths on Unix platforms r=mayhemer
authorValentin Gosu <valentin.gosu@gmail.com>
Wed, 20 Jun 2018 02:52:12 +0200
changeset 818898 9ec0f327c5efc38970c090015eaf11a7e5c450b7
parent 818897 5f39a82a042ea13782f74e33e800be2783cea0a4
child 818899 26163df1083ea2847022e60518ab973360b9d7c1
push id116388
push userrwood@mozilla.com
push dateMon, 16 Jul 2018 19:48:57 +0000
reviewersmayhemer
bugs1412081
milestone63.0a1
Bug 1412081 - Add ability to blacklist file paths on Unix platforms r=mayhemer
xpcom/io/FilePreferences.cpp
xpcom/io/FilePreferences.h
xpcom/io/nsLocalFileUnix.cpp
xpcom/tests/gtest/TestFilePreferencesUnix.cpp
xpcom/tests/gtest/moz.build
--- a/xpcom/io/FilePreferences.cpp
+++ b/xpcom/io/FilePreferences.cpp
@@ -1,35 +1,56 @@
 /* -*- 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/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/StaticPtr.h"
 #include "mozilla/Tokenizer.h"
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsDirectoryServiceUtils.h"
+#include "nsString.h"
 
 namespace mozilla {
 namespace FilePreferences {
 
 static bool sBlockUNCPaths = false;
-typedef nsTArray<nsString> Paths;
+typedef nsTArray<nsString> WinPaths;
 
-static Paths& PathArray()
+static WinPaths& PathWhitelist()
 {
-  static Paths sPaths;
+  static WinPaths sPaths;
   return sPaths;
 }
 
-static void AllowDirectory(char const* directory)
+#ifdef XP_WIN
+typedef char16_t char_path_t;
+#else
+typedef char char_path_t;
+#endif
+
+typedef nsTArray<nsTString<char_path_t>> Paths;
+static StaticAutoPtr<Paths> sBlacklist;
+
+static Paths& PathBlacklist()
+{
+  if (!sBlacklist) {
+    sBlacklist = new nsTArray<nsTString<char_path_t>>();
+    ClearOnShutdown(&sBlacklist);
+  }
+  return *sBlacklist;
+}
+
+static void AllowUNCDirectory(char const* directory)
 {
   nsCOMPtr<nsIFile> file;
   NS_GetSpecialDirectory(directory, getter_AddRefs(file));
   if (!file) {
     return;
   }
 
   nsString path;
@@ -39,160 +60,185 @@ static void AllowDirectory(char const* d
 
   // The whitelist 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, NS_LITERAL_STRING("\\\\"))) {
     return;
   }
 
-  if (!PathArray().Contains(path)) {
-    PathArray().AppendElement(path);
+  if (!PathWhitelist().Contains(path)) {
+    PathWhitelist().AppendElement(path);
   }
 }
 
 void InitPrefs()
 {
   sBlockUNCPaths = Preferences::GetBool("network.file.disable_unc_paths", false);
+
+  PathBlacklist().Clear();
+  nsTAutoString<char_path_t> blacklist;
+#ifdef XP_WIN
+  Preferences::GetString("network.file.path_blacklist", blacklist);
+#else
+  Preferences::GetCString("network.file.path_blacklist", blacklist);
+#endif
+
+  TTokenizer<char_path_t> p(blacklist);
+  while (!p.CheckEOF()) {
+    nsTString<char_path_t> path;
+    Unused << p.ReadUntil(TTokenizer<char_path_t>::Token::Char(','), path);
+    path.Trim(" ");
+    if (!path.IsEmpty()) {
+      PathBlacklist().AppendElement(path);
+    }
+    Unused << p.CheckChar(',');
+  }
 }
 
 void InitDirectoriesWhitelist()
 {
   // NS_GRE_DIR is the installation path where the binary resides.
-  AllowDirectory(NS_GRE_DIR);
+  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.
-  AllowDirectory(NS_APP_USER_PROFILE_50_DIR);
-  AllowDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR);
+  AllowUNCDirectory(NS_APP_USER_PROFILE_50_DIR);
+  AllowUNCDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR);
 }
 
 namespace { // anon
 
-class Normalizer : public Tokenizer16
+template <typename TChar>
+class TNormalizer
+  : public TTokenizer<TChar>
 {
+  typedef TTokenizer<TChar> base;
 public:
-  Normalizer(const nsAString& aFilePath, const Token& aSeparator);
-  bool Get(nsAString& aNormalizedFilePath);
-
-private:
-  bool ConsumeName();
-  bool CheckParentDir();
-  bool CheckCurrentDir();
-  bool CheckSeparator();
-
-  Token const mSeparator;
-  nsTArray<nsDependentSubstring> mStack;
-};
+  typedef typename base::Token Token;
 
-Normalizer::Normalizer(const nsAString& aFilePath, const Token& aSeparator)
-  : Tokenizer16(aFilePath)
-  , mSeparator(aSeparator)
-{
-}
-
-bool Normalizer::Get(nsAString& aNormalizedFilePath)
-{
-  aNormalizedFilePath.Truncate();
-
-  if (Check(mSeparator)) {
-    aNormalizedFilePath.Append(mSeparator.AsChar());
-  }
-  if (Check(mSeparator)) {
-    aNormalizedFilePath.Append(mSeparator.AsChar());
+  TNormalizer(const nsTSubstring<TChar>& aFilePath, const Token& aSeparator)
+    : TTokenizer<TChar>(aFilePath)
+    , mSeparator(aSeparator)
+  {
   }
 
-  while (HasInput()) {
-    if (!ConsumeName()) {
-      return false;
+  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
 
-  for (auto const& name : mStack) {
-    aNormalizedFilePath.Append(name);
-  }
+    if (base::Check(mSeparator)) {
+      aNormalizedFilePath.Append(mSeparator.AsChar());
+    }
 
-  return true;
-}
+    while (base::HasInput()) {
+      if (!ConsumeName()) {
+        return false;
+      }
+    }
 
-bool Normalizer::ConsumeName()
-{
-  if (CheckEOF()) {
+    for (auto const& name : mStack) {
+      aNormalizedFilePath.Append(name);
+    }
+
     return true;
   }
 
-  if (CheckCurrentDir()) {
-    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
+    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 and empty name (a lone slash), which is illegal
       return false;
     }
+    mStack.AppendElement(name);
 
-    mStack.RemoveLastElement();
     return true;
   }
 
-  nsDependentSubstring name;
-  if (ReadUntil(mSeparator, name, INCLUDE_LAST) && name.Length() == 1) {
-    // this means and empty name (a lone slash), which is illegal
-    return false;
-  }
-  mStack.AppendElement(name);
+  bool CheckParentDir()
+  {
+    typename nsTString<TChar>::const_char_iterator cursor = base::mCursor;
+    if (base::CheckChar('.') && base::CheckChar('.') && CheckSeparator()) {
+      return true;
+    }
 
-  return true;
-}
-
-bool Normalizer::CheckCurrentDir()
-{
-  nsString::const_char_iterator cursor = mCursor;
-  if (CheckChar('.') && CheckSeparator()) {
-    return true;
+    base::mCursor = cursor;
+    return false;
   }
 
-  mCursor = cursor;
-  return false;
-}
+  bool CheckCurrentDir()
+  {
+    typename nsTString<TChar>::const_char_iterator cursor = base::mCursor;
+    if (base::CheckChar('.') && CheckSeparator()) {
+      return true;
+    }
 
-bool Normalizer::CheckParentDir()
-{
-  nsString::const_char_iterator cursor = mCursor;
-  if (CheckChar('.') && CheckChar('.') && CheckSeparator()) {
-    return true;
+    base::mCursor = cursor;
+    return false;
   }
 
-  mCursor = cursor;
-  return false;
-}
+  bool CheckSeparator()
+  {
+    return base::Check(mSeparator) || base::CheckEOF();
+  }
 
-bool Normalizer::CheckSeparator()
-{
-  return Check(mSeparator) || CheckEOF();
-}
+  Token const mSeparator;
+  nsTArray<nsTDependentSubstring<TChar>> mStack;
+};
 
 } // anon
 
 bool IsBlockedUNCPath(const nsAString& aFilePath)
 {
+  typedef TNormalizer<char16_t> Normalizer;
   if (!sBlockUNCPaths) {
     return false;
   }
 
   if (!StringBeginsWith(aFilePath, NS_LITERAL_STRING("\\\\"))) {
     return false;
   }
 
   nsAutoString normalized;
   if (!Normalizer(aFilePath, Normalizer::Token::Char('\\')).Get(normalized)) {
     // Broken paths are considered invalid and thus inaccessible
     return true;
   }
 
-  for (const auto& allowedPrefix : PathArray()) {
+  for (const auto& allowedPrefix : PathWhitelist()) {
     if (StringBeginsWith(normalized, allowedPrefix)) {
       if (normalized.Length() == allowedPrefix.Length()) {
         return false;
       }
       if (normalized[allowedPrefix.Length()] == L'\\') {
         return false;
       }
 
@@ -202,26 +248,65 @@ bool IsBlockedUNCPath(const nsAString& a
       // 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;
+  // If sBlacklist has been cleared at shutdown, we must avoid calling
+  // PathBlacklist() again, as that will recreate the array and we will leak.
+  if (!sBlacklist) {
+    return true;
+  }
+
+  if (PathBlacklist().Length() == 0) {
+    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 : PathBlacklist()) {
+    if (StringBeginsWith(normalized, prefix)) {
+      if (normalized.Length() > prefix.Length() &&
+          normalized[prefix.Length()] != kPathSeparator) {
+        continue;
+      }
+      return false;
+    }
+  }
+
+  return true;
+}
+
 void testing::SetBlockUNCPaths(bool aBlock)
 {
   sBlockUNCPaths = aBlock;
 }
 
 void testing::AddDirectoryToWhitelist(nsAString const & aPath)
 {
-  PathArray().AppendElement(aPath);
+  PathWhitelist().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);
 }
 
 } // ::FilePreferences
 } // ::mozilla
--- a/xpcom/io/FilePreferences.h
+++ b/xpcom/io/FilePreferences.h
@@ -8,16 +8,22 @@
 
 namespace mozilla {
 namespace FilePreferences {
 
 void InitPrefs();
 void InitDirectoriesWhitelist();
 bool IsBlockedUNCPath(const nsAString& aFilePath);
 
+#ifdef XP_WIN
+bool IsAllowedPath(const nsAString& aFilePath);
+#else
+bool IsAllowedPath(const nsACString& aFilePath);
+#endif
+
 namespace testing {
 
 void SetBlockUNCPaths(bool aBlock);
 void AddDirectoryToWhitelist(nsAString const& aPath);
 bool NormalizePath(nsAString const & aPath, nsAString & aNormalized);
 
 }
 
--- a/xpcom/io/nsLocalFileUnix.cpp
+++ b/xpcom/io/nsLocalFileUnix.cpp
@@ -7,16 +7,17 @@
 /**
  * Implementation of nsIFile for "unixy" systems.
  */
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Sprintf.h"
+#include "mozilla/FilePreferences.h"
 
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <errno.h>
 #include <utime.h>
 #include <dirent.h>
@@ -79,16 +80,18 @@ using namespace mozilla;
         if (!FillStatCache())                   \
              return NSRESULT_FOR_ERRNO();       \
     } while(0)
 
 #define CHECK_mPath()                           \
     do {                                        \
         if (mPath.IsEmpty())                    \
             return NS_ERROR_NOT_INITIALIZED;    \
+        if (!FilePreferences::IsAllowedPath(mPath)) \
+            return NS_ERROR_FILE_ACCESS_DENIED; \
     } while(0)
 
 /* directory enumerator */
 class nsDirEnumeratorUnix final
   : public nsIDirectoryEnumerator
 {
 public:
   nsDirEnumeratorUnix();
@@ -134,16 +137,23 @@ nsDirEnumeratorUnix::Init(nsLocalFile* a
                           bool aResolveSymlinks /*ignored*/)
 {
   nsAutoCString dirPath;
   if (NS_FAILED(aParent->GetNativePath(dirPath)) ||
       dirPath.IsEmpty()) {
     return NS_ERROR_FILE_INVALID_PATH;
   }
 
+  // When enumerating the directory, the paths must have a slash at the end.
+  nsAutoCString dirPathWithSlash(dirPath);
+  dirPathWithSlash.Append('/');
+  if (!FilePreferences::IsAllowedPath(dirPathWithSlash)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   if (NS_FAILED(aParent->GetNativePath(mParentPath))) {
     return NS_ERROR_FAILURE;
   }
 
   mDir = opendir(dirPath.get());
   if (!mDir) {
     return NSRESULT_FOR_ERRNO();
   }
@@ -265,16 +275,21 @@ nsLocalFile::nsLocalFileConstructor(nsIS
 
   nsCOMPtr<nsIFile> inst = new nsLocalFile();
   return inst->QueryInterface(aIID, aInstancePtr);
 }
 
 bool
 nsLocalFile::FillStatCache()
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    errno = EACCES;
+    return false;
+  }
+
   if (STAT(mPath.get(), &mCachedStat) == -1) {
     // try lstat it may be a symlink
     if (LSTAT(mPath.get(), &mCachedStat) == -1) {
       return false;
     }
   }
   return true;
 }
@@ -307,29 +322,38 @@ nsLocalFile::InitWithNativePath(const ns
     }
   } else {
     if (aFilePath.IsEmpty() || aFilePath.First() != '/') {
       return NS_ERROR_FILE_UNRECOGNIZED_PATH;
     }
     mPath = aFilePath;
   }
 
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    mPath.Truncate();
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   // trim off trailing slashes
   ssize_t len = mPath.Length();
   while ((len > 1) && (mPath[len - 1] == '/')) {
     --len;
   }
   mPath.SetLength(len);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsLocalFile::CreateAllAncestors(uint32_t aPermissions)
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   // <jband> I promise to play nice
   char* buffer = mPath.BeginWriting();
   char* slashp = buffer;
 
 #ifdef DEBUG_NSIFILE
   fprintf(stderr, "nsIFile: before: %s\n", buffer);
 #endif
 
@@ -391,16 +415,19 @@ nsLocalFile::CreateAllAncestors(uint32_t
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsLocalFile::OpenNSPRFileDesc(int32_t aFlags, int32_t aMode,
                               PRFileDesc** aResult)
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
   *aResult = PR_Open(mPath.get(), aFlags, aMode);
   if (!*aResult) {
     return NS_ErrorAccordingToNSPR();
   }
 
   if (aFlags & DELETE_ON_CLOSE) {
     PR_Delete(mPath.get());
   }
@@ -412,16 +439,19 @@ nsLocalFile::OpenNSPRFileDesc(int32_t aF
   }
 #endif
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsLocalFile::OpenANSIFileDesc(const char* aMode, FILE** aResult)
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
   *aResult = fopen(mPath.get(), aMode);
   if (!*aResult) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
@@ -438,16 +468,20 @@ do_mkdir(const char* aPath, int aFlags, 
   *aResult = nullptr;
   return mkdir(aPath, aMode);
 }
 
 nsresult
 nsLocalFile::CreateAndKeepOpen(uint32_t aType, int aFlags,
                                uint32_t aPermissions, PRFileDesc** aResult)
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   if (aType != NORMAL_FILE_TYPE && aType != DIRECTORY_TYPE) {
     return NS_ERROR_FILE_UNKNOWN_TYPE;
   }
 
   int (*createFunc)(const char*, int, mode_t, PRFileDesc**) =
     (aType == NORMAL_FILE_TYPE) ? do_create : do_mkdir;
 
   int result = createFunc(mPath.get(), aFlags, aPermissions, aResult);
@@ -487,16 +521,20 @@ nsLocalFile::CreateAndKeepOpen(uint32_t 
     result = createFunc(mPath.get(), aFlags, aPermissions, aResult);
   }
   return NSRESULT_FOR_RETURN(result);
 }
 
 NS_IMETHODIMP
 nsLocalFile::Create(uint32_t aType, uint32_t aPermissions)
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   PRFileDesc* junk = nullptr;
   nsresult rv = CreateAndKeepOpen(aType,
                                   PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE |
                                   PR_EXCL,
                                   aPermissions,
                                   &junk);
   if (junk) {
     PR_Close(junk);
@@ -542,16 +580,20 @@ nsLocalFile::AppendRelativeNativePath(co
 }
 
 NS_IMETHODIMP
 nsLocalFile::Normalize()
 {
   char resolved_path[PATH_MAX] = "";
   char* resolved_path_ptr = nullptr;
 
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   resolved_path_ptr = realpath(mPath.get(), resolved_path);
 
   // if there is an error, the return is null.
   if (!resolved_path_ptr) {
     return NSRESULT_FOR_ERRNO();
   }
 
   mPath = resolved_path;
@@ -1006,16 +1048,20 @@ nsLocalFile::MoveToNative(nsIFile* aNewP
 
   // check to make sure that we have a new parent
   nsAutoCString newPathName;
   rv = GetNativeTargetPathName(aNewParent, aNewName, newPathName);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
+  if (!FilePreferences::IsAllowedPath(newPathName)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   // try for atomic rename, falling back to copy/delete
   if (rename(mPath.get(), newPathName.get()) < 0) {
     if (errno == EXDEV) {
       rv = CopyToNative(aNewParent, aNewName);
       if (NS_SUCCEEDED(rv)) {
         rv = Remove(true);
       }
     } else {
@@ -1948,16 +1994,20 @@ nsLocalFile::SetPersistentDescriptor(con
 #else
   return InitWithNativePath(aPersistentDescriptor);
 #endif
 }
 
 NS_IMETHODIMP
 nsLocalFile::Reveal()
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
 #ifdef MOZ_WIDGET_GTK
   nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
   if (!giovfs) {
     return NS_ERROR_FAILURE;
   }
 
   bool isDirectory;
   if (NS_FAILED(IsDirectory(&isDirectory))) {
@@ -1991,16 +2041,20 @@ nsLocalFile::Reveal()
 #else
   return NS_ERROR_FAILURE;
 #endif
 }
 
 NS_IMETHODIMP
 nsLocalFile::Launch()
 {
+  if (!FilePreferences::IsAllowedPath(mPath)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
 #ifdef MOZ_WIDGET_GTK
   nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
   if (!giovfs) {
     return NS_ERROR_FAILURE;
   }
 
   return giovfs->ShowURIForInput(mPath);
 #elif defined(MOZ_WIDGET_ANDROID)
@@ -2145,16 +2199,20 @@ nsLocalFile::RenameToNative(nsIFile* aNe
 
   // check to make sure that we have a new parent
   nsAutoCString newPathName;
   rv = GetNativeTargetPathName(aNewParentDir, aNewName, newPathName);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
+  if (!FilePreferences::IsAllowedPath(newPathName)) {
+    return NS_ERROR_FILE_ACCESS_DENIED;
+  }
+
   // try for atomic rename
   if (rename(mPath.get(), newPathName.get()) < 0) {
     if (errno == EXDEV) {
       rv = NS_ERROR_FILE_ACCESS_DENIED;
     } else {
       rv = NSRESULT_FOR_ERRNO();
     }
   }
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/gtest/TestFilePreferencesUnix.cpp
@@ -0,0 +1,203 @@
+#include "gtest/gtest.h"
+
+#include "mozilla/FilePreferences.h"
+
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "nsISimpleEnumerator.h"
+
+using namespace mozilla;
+
+TEST(TestFilePreferencesUnix, Parsing)
+{
+  #define kBlacklisted "/tmp/blacklisted"
+  #define kBlacklistedDir "/tmp/blacklisted/"
+  #define kBlacklistedFile "/tmp/blacklisted/file"
+  #define kOther "/tmp/other"
+  #define kOtherDir "/tmp/other/"
+  #define kOtherFile "/tmp/other/file"
+  #define kAllowed "/tmp/allowed"
+
+  // This is run on exit of this function to make sure we clear the pref
+  // and that behaviour with the pref cleared is correct.
+  auto cleanup = MakeScopeExit([&] {
+    nsresult rv = Preferences::ClearUser("network.file.path_blacklist");
+    ASSERT_EQ(rv, NS_OK);
+    FilePreferences::InitPrefs();
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklisted)), true);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklistedDir)), true);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklistedFile)), true);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kAllowed)), true);
+  });
+
+  auto CheckPrefs = [](const nsACString& aPaths)
+  {
+    nsresult rv;
+    rv = Preferences::SetCString("network.file.path_blacklist", aPaths);
+    ASSERT_EQ(rv, NS_OK);
+    FilePreferences::InitPrefs();
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklistedDir)), false);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklistedDir)), false);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklistedFile)), false);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kBlacklisted)), false);
+    ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kAllowed)), true);
+  };
+
+  CheckPrefs(NS_LITERAL_CSTRING(kBlacklisted));
+  CheckPrefs(NS_LITERAL_CSTRING(kBlacklisted "," kOther));
+  ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kOtherFile)), false);
+  CheckPrefs(NS_LITERAL_CSTRING(kBlacklisted "," kOther ","));
+  ASSERT_EQ(FilePreferences::IsAllowedPath(NS_LITERAL_CSTRING(kOtherFile)), false);
+}
+
+TEST(TestFilePreferencesUnix, Simple)
+{
+  nsAutoCString tempPath;
+
+  // This is the directory we will blacklist
+  nsCOMPtr<nsIFile> blacklistedDir;
+  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(blacklistedDir));
+  ASSERT_EQ(rv, NS_OK);
+  rv = blacklistedDir->GetNativePath(tempPath);
+  ASSERT_EQ(rv, NS_OK);
+  rv = blacklistedDir->AppendNative(NS_LITERAL_CSTRING("blacklisted_dir"));
+  ASSERT_EQ(rv, NS_OK);
+
+  // This is executed at exit to clean up after ourselves.
+  auto cleanup = MakeScopeExit([&] {
+    nsresult rv = Preferences::ClearUser("network.file.path_blacklist");
+    ASSERT_EQ(rv, NS_OK);
+    FilePreferences::InitPrefs();
+
+    rv = blacklistedDir->Remove(true);
+    ASSERT_EQ(rv, NS_OK);
+  });
+
+  // Create the directory
+  rv = blacklistedDir->Create(nsIFile::DIRECTORY_TYPE, 0666);
+  ASSERT_EQ(rv, NS_OK);
+
+  // This is the file we will try to access
+  nsCOMPtr<nsIFile> blacklistedFile;
+  rv = blacklistedDir->Clone(getter_AddRefs(blacklistedFile));
+  ASSERT_EQ(rv, NS_OK);
+  rv = blacklistedFile->AppendNative(NS_LITERAL_CSTRING("test_file"));
+
+  // Create the file
+  ASSERT_EQ(rv, NS_OK);
+  rv = blacklistedFile->Create(nsIFile::NORMAL_FILE_TYPE, 0666);
+
+  // Get the path for the blacklist
+  nsAutoCString blackListPath;
+  rv = blacklistedDir->GetNativePath(blackListPath);
+  ASSERT_EQ(rv, NS_OK);
+
+  // Set the pref and make sure it is enforced
+  rv = Preferences::SetCString("network.file.path_blacklist", blackListPath);
+  ASSERT_EQ(rv, NS_OK);
+  FilePreferences::InitPrefs();
+
+  // Check that we can't access some of the file attributes
+  int64_t size;
+  rv = blacklistedFile->GetFileSize(&size);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  bool exists;
+  rv = blacklistedFile->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  // Check that we can't enumerate the directory
+  nsCOMPtr<nsISimpleEnumerator> dirEnumerator;
+  rv = blacklistedDir->GetDirectoryEntries(getter_AddRefs(dirEnumerator));
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  nsCOMPtr<nsIFile> newPath;
+  rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(newPath));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING("."));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING("blacklisted_dir"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING("test_file"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  // Check that ./ does not bypass the filter
+  rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(newPath));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendRelativeNativePath(NS_LITERAL_CSTRING("./blacklisted_dir/file"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  // Check that ..  does not bypass the filter
+  rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(newPath));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendRelativeNativePath(NS_LITERAL_CSTRING("allowed/../blacklisted_dir/file"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(newPath));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING("allowed"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING(".."));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->AppendNative(NS_LITERAL_CSTRING("blacklisted_dir"));
+  ASSERT_EQ(rv, NS_OK);
+  rv = newPath->Exists(&exists);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  nsAutoCString trickyPath(tempPath);
+  trickyPath.AppendLiteral("/allowed/../blacklisted_dir/file");
+  rv = newPath->InitWithNativePath(trickyPath);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  // Check that we can't construct a path that is functionally the same
+  // as the blacklisted one and bypasses the filter.
+  trickyPath = tempPath;
+  trickyPath.AppendLiteral("/./blacklisted_dir/file");
+  rv = newPath->InitWithNativePath(trickyPath);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  trickyPath = tempPath;
+  trickyPath.AppendLiteral("//blacklisted_dir/file");
+  rv = newPath->InitWithNativePath(trickyPath);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  trickyPath.Truncate();
+  trickyPath.AppendLiteral("//");
+  trickyPath.Append(tempPath);
+  trickyPath.AppendLiteral("/blacklisted_dir/file");
+  rv = newPath->InitWithNativePath(trickyPath);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  trickyPath.Truncate();
+  trickyPath.AppendLiteral("//");
+  trickyPath.Append(tempPath);
+  trickyPath.AppendLiteral("//blacklisted_dir/file");
+  rv = newPath->InitWithNativePath(trickyPath);
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+
+  // Check that if the blacklisted string is a directory, we only block access
+  // to subresources, not the directory itself.
+  nsAutoCString blacklistDirPath(blackListPath);
+  blacklistDirPath.Append("/");
+  rv = Preferences::SetCString("network.file.path_blacklist", blacklistDirPath);
+  ASSERT_EQ(rv, NS_OK);
+  FilePreferences::InitPrefs();
+
+  // This should work, since we only block subresources
+  rv = blacklistedDir->Exists(&exists);
+  ASSERT_EQ(rv, NS_OK);
+
+  rv = blacklistedDir->GetDirectoryEntries(getter_AddRefs(dirEnumerator));
+  ASSERT_EQ(rv, NS_ERROR_FILE_ACCESS_DENIED);
+}
--- a/xpcom/tests/gtest/moz.build
+++ b/xpcom/tests/gtest/moz.build
@@ -71,16 +71,21 @@ if CONFIG['MOZ_DEBUG'] and CONFIG['OS_AR
         'TestDeadlockDetector.cpp',
         'TestDeadlockDetectorScalability.cpp',
     ]
 
 if CONFIG['OS_TARGET'] == 'WINNT':
     UNIFIED_SOURCES += [
         'TestFilePreferencesWin.cpp',
     ]
+else:
+    UNIFIED_SOURCES += [
+        'TestFilePreferencesUnix.cpp',
+    ]
+
 
 if CONFIG['WRAP_STL_INCLUDES'] and CONFIG['CC_TYPE'] != 'clang-cl':
     UNIFIED_SOURCES += [
         'TestSTLWrappers.cpp',
     ]
 
 # Compile TestAllocReplacement separately so Windows headers don't pollute
 # the global namespace for other files.