Bug 995943 - Allow access to file:// URIs from pref-whitelisted sites. r=bz a=lsblakk
authorBobby Holley <bobbyholley@gmail.com>
Mon, 12 May 2014 15:31:22 -0700
changeset 192254 5b3bfd0a529a
parent 192253 c1b7c8d03129
child 192255 e414a4798c7c
push id3546
push userbobbyholley@gmail.com
push date2014-05-12 22:31 +0000
Treeherderresults
reviewersbz, lsblakk
bugs995943
milestone30.0
Bug 995943 - Allow access to file:// URIs from pref-whitelisted sites. r=bz a=lsblakk
caps/include/nsScriptSecurityManager.h
caps/src/nsScriptSecurityManager.cpp
caps/tests/mochitest/chrome.ini
caps/tests/mochitest/test_bug995943.xul
--- a/caps/include/nsScriptSecurityManager.h
+++ b/caps/include/nsScriptSecurityManager.h
@@ -18,17 +18,17 @@
 #include "pldhash.h"
 #include "plstr.h"
 #include "nsIScriptExternalNameSet.h"
 #include "js/TypeDecls.h"
 
 #include <stdint.h>
 
 class nsIDocShell;
-class nsString;
+class nsCString;
 class nsIClassInfo;
 class nsIIOService;
 class nsIStringBundle;
 class nsSystemPrincipal;
 class ClassInfoData;
 
 /////////////////////////////
 // nsScriptSecurityManager //
@@ -145,19 +145,23 @@ private:
     Init();
 
     nsresult
     InitPrefs();
 
     inline void
     ScriptSecurityPrefChanged();
 
+    inline void
+    AddSitesToFileURIWhitelist(const nsCString& aSiteList);
+
     nsCOMPtr<nsIPrincipal> mSystemPrincipal;
     bool mPrefInitialized;
     bool mIsJavaScriptEnabled;
+    nsTArray<nsCOMPtr<nsIURI>> mFileURIWhitelist;
 
     // This machinery controls new-style domain policies. The old-style
     // policy machinery will be removed soon.
     nsCOMPtr<nsIDomainPolicy> mDomainPolicy;
 
     static bool sStrictFileOriginPolicy;
 
     static nsIIOService    *sIOService;
--- a/caps/src/nsScriptSecurityManager.cpp
+++ b/caps/src/nsScriptSecurityManager.cpp
@@ -775,16 +775,24 @@ nsScriptSecurityManager::CheckLoadURIWit
     }
 
     // Check for target URI pointing to a file
     rv = NS_URIChainHasFlags(targetBaseURI,
                              nsIProtocolHandler::URI_IS_LOCAL_FILE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
     if (hasFlags) {
+        // Allow domains that were whitelisted in the prefs. In 99.9% of cases,
+        // this array is empty.
+        for (size_t i = 0; i < mFileURIWhitelist.Length(); ++i) {
+            if (SecurityCompareURIs(mFileURIWhitelist[i], sourceURI)) {
+                return NS_OK;
+            }
+        }
+
         // resource: and chrome: are equivalent, securitywise
         // That's bogus!!  Fix this.  But watch out for
         // the view-source stylesheet?
         bool sourceIsChrome;
         rv = NS_URIChainHasFlags(sourceURI,
                                  nsIProtocolHandler::URI_IS_UI_RESOURCE,
                                  &sourceIsChrome);
         NS_ENSURE_SUCCESS(rv, rv);
@@ -1263,16 +1271,17 @@ nsScriptSecurityManager::AsyncOnChannelR
 /////////////////////////////////////
 const char sJSEnabledPrefName[] = "javascript.enabled";
 const char sFileOriginPolicyPrefName[] =
     "security.fileuri.strict_origin_policy";
 
 static const char* kObservedPrefs[] = {
   sJSEnabledPrefName,
   sFileOriginPolicyPrefName,
+  "capability.policy.",
   nullptr
 };
 
 
 NS_IMETHODIMP
 nsScriptSecurityManager::Observe(nsISupports* aObject, const char* aTopic,
                                  const char16_t* aMessage)
 {
@@ -1388,24 +1397,108 @@ nsSystemPrincipal *
 nsScriptSecurityManager::SystemPrincipalSingletonConstructor()
 {
     nsIPrincipal *sysprin = nullptr;
     if (gScriptSecMan)
         NS_ADDREF(sysprin = gScriptSecMan->mSystemPrincipal);
     return static_cast<nsSystemPrincipal*>(sysprin);
 }
 
+struct IsWhitespace {
+    static bool Test(char aChar) { return NS_IsAsciiWhitespace(aChar); };
+};
+struct IsWhitespaceOrComma {
+    static bool Test(char aChar) { return aChar == ',' || NS_IsAsciiWhitespace(aChar); };
+};
+
+template <typename Predicate>
+uint32_t SkipPast(const nsCString& str, uint32_t base)
+{
+    while (base < str.Length() && Predicate::Test(str[base])) {
+        ++base;
+    }
+    return base;
+}
+
+template <typename Predicate>
+uint32_t SkipUntil(const nsCString& str, uint32_t base)
+{
+    while (base < str.Length() && !Predicate::Test(str[base])) {
+        ++base;
+    }
+    return base;
+}
+
 inline void
 nsScriptSecurityManager::ScriptSecurityPrefChanged()
 {
     MOZ_ASSERT(mPrefInitialized);
     mIsJavaScriptEnabled =
         Preferences::GetBool(sJSEnabledPrefName, mIsJavaScriptEnabled);
     sStrictFileOriginPolicy =
         Preferences::GetBool(sFileOriginPolicyPrefName, false);
+
+    //
+    // Rebuild the set of principals for which we allow file:// URI loads. This
+    // implements a small subset of an old pref-based CAPS people that people
+    // have come to depend on. See bug 995943.
+    //
+
+    mFileURIWhitelist.Clear();
+    auto policies = mozilla::Preferences::GetCString("capability.policy.policynames");
+    for (uint32_t base = SkipPast<IsWhitespaceOrComma>(policies, 0), bound = 0;
+         base < policies.Length();
+         base = SkipPast<IsWhitespaceOrComma>(policies, bound))
+    {
+        // Grab the current policy name.
+        bound = SkipUntil<IsWhitespaceOrComma>(policies, base);
+        auto policyName = Substring(policies, base, bound - base);
+
+        // Figure out if this policy allows loading file:// URIs. If not, we can skip it.
+        nsCString checkLoadURIPrefName = NS_LITERAL_CSTRING("capability.policy.") +
+                                         policyName +
+                                         NS_LITERAL_CSTRING(".checkloaduri.enabled");
+        if (!Preferences::GetString(checkLoadURIPrefName.get()).LowerCaseEqualsLiteral("allaccess")) {
+            continue;
+        }
+
+        // Grab the list of domains associated with this policy.
+        nsCString domainPrefName = NS_LITERAL_CSTRING("capability.policy.") +
+                                   policyName +
+                                   NS_LITERAL_CSTRING(".sites");
+        auto siteList = Preferences::GetCString(domainPrefName.get());
+        AddSitesToFileURIWhitelist(siteList);
+    }
+}
+
+void
+nsScriptSecurityManager::AddSitesToFileURIWhitelist(const nsCString& aSiteList)
+{
+    for (uint32_t base = SkipPast<IsWhitespace>(aSiteList, 0), bound = 0;
+         base < aSiteList.Length();
+         base = SkipPast<IsWhitespace>(aSiteList, bound))
+    {
+        // Grab the current site.
+        bound = SkipUntil<IsWhitespace>(aSiteList, base);
+        auto site = Substring(aSiteList, base, bound - base);
+
+        // Convert it to a URI and add it to our list.
+        nsCOMPtr<nsIURI> uri;
+        nsresult rv = NS_NewURI(getter_AddRefs(uri), site, nullptr, nullptr, sIOService);
+        if (NS_SUCCEEDED(rv)) {
+            mFileURIWhitelist.AppendElement(uri);
+        } else {
+            nsCOMPtr<nsIConsoleService> console(do_GetService("@mozilla.org/consoleservice;1"));
+            if (console) {
+                nsAutoString msg = NS_LITERAL_STRING("Unable to to add site to file:// URI whitelist: ") +
+                                   NS_ConvertASCIItoUTF16(site);
+                console->LogStringMessage(msg.get());
+            }
+        }
+    }
 }
 
 nsresult
 nsScriptSecurityManager::InitPrefs()
 {
     nsIPrefBranch* branch = Preferences::GetRootBranch();
     NS_ENSURE_TRUE(branch, NS_ERROR_FAILURE);
 
--- a/caps/tests/mochitest/chrome.ini
+++ b/caps/tests/mochitest/chrome.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 support-files =
   file_disableScript.html
 
+[test_bug995943.xul]
 [test_disableScript.xul]
 [test_principal_jarprefix_origin_appid_appstatus.html]
 # jarPrefix test doesn't work on Windows, see bug 776296.
 skip-if = os == "win"
new file mode 100644
--- /dev/null
+++ b/caps/tests/mochitest/test_bug995943.xul
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=995943
+-->
+<window title="Mozilla Bug 995943"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=995943"
+     target="_blank">Mozilla Bug 995943</a>
+  </body>
+
+  <!-- test code goes here -->
+  <script type="application/javascript">
+  <![CDATA[
+  const Cu = Components.utils;
+  const Cc = Components.classes;
+  const Ci = Components.interfaces;
+  Cu.import("resource://gre/modules/Services.jsm");
+  function debug(msg) { info(msg); }
+
+  /** Test for CAPS file:// URI prefs. **/
+  SimpleTest.waitForExplicitFinish();
+
+  var profileDir = "file://" + Cc["@mozilla.org/file/directory_service;1"]
+                               .getService(Ci.nsIProperties)
+                               .get("ProfD", Ci.nsILocalFile).path;
+
+  function checkLoadFileURI(domain, shouldLoad) {
+    debug("Invoking checkLoadFileURI with domain: " + domain + ", shouldLoad: " + shouldLoad);
+    return new Promise(function(resolve, reject) {
+      $('ifr').addEventListener('load', function l1() {
+        $('ifr').removeEventListener('load', l1);
+        function l2() {
+          $('ifr').removeEventListener('load', l2);
+          ok(shouldLoad, "Successfully loaded file:// URI for domain: " + domain);
+          resolve();
+        }
+        $('ifr').addEventListener('load', l2);
+        try {
+          window[0].wrappedJSObject.location = profileDir;
+        } catch (e) {
+          ok(!shouldLoad && /denied|insecure/.test(e),
+             "Prevented loading of file:// URI for domain: " + domain + " - " + e);
+          $('ifr').removeEventListener('load', l2);
+          resolve();
+        }
+      });
+      let targetURI = domain + '/tests/js/xpconnect/tests/mochitest/file_empty.html';
+      debug("Navigating iframe to " + targetURI);
+      $('ifr').contentWindow.location = targetURI;
+    });
+  }
+
+  function pushPrefs(prefs) {
+    return new Promise(function(resolve) { SpecialPowers.pushPrefEnv({ set: prefs }, resolve); });
+  }
+
+  function popPrefs() {
+    return new Promise(function(resolve) { SpecialPowers.popPrefEnv(resolve); });
+  }
+
+  function go() {
+    checkLoadFileURI('http://example.com', false).then(
+      pushPrefs.bind(null, [['capability.policy.policynames', ' somepolicy '],
+                            ['capability.policy.somepolicy.checkloaduri.enabled', 'AlLAcCeSs'],
+                            ['capability.policy.somepolicy.sites', 'http://example.com']]))
+    .then(checkLoadFileURI.bind(null, 'http://example.com', true))
+    .then(popPrefs)
+    .then(checkLoadFileURI.bind(null, 'http://example.com', false))
+    .then(
+      pushPrefs.bind(null, [['capability.policy.policynames', ',somepolicy, someotherpolicy, '],
+                            ['capability.policy.somepolicy.checkloaduri.enabled', 'allaccess'],
+                            ['capability.policy.someotherpolicy.checkloaduri.enabled', 'nope'],
+                            ['capability.policy.somepolicy.sites', ' http://example.org   https://example.com'],
+                            ['capability.policy.someotherpolicy.sites', 'http://example.net ']]))
+    .then(checkLoadFileURI.bind(null, 'http://example.org', true))
+    .then(checkLoadFileURI.bind(null, 'http://example.com', false))
+    .then(checkLoadFileURI.bind(null, 'https://example.com', true))
+    .then(checkLoadFileURI.bind(null, 'http://example.net', false))
+    .then(pushPrefs.bind(null, [['capability.policy.someotherpolicy.checkloaduri.enabled', 'allAccess']]))
+    .then(checkLoadFileURI.bind(null, 'http://example.net', true))
+    .then(popPrefs)
+    .then(popPrefs)
+    .then(checkLoadFileURI.bind(null, 'http://example.net', false))
+    .then(SimpleTest.finish.bind(SimpleTest));
+
+  }
+  addLoadEvent(go);
+
+  ]]>
+  </script>
+  <iframe id="ifr" type="content" />
+</window>