Bug 879658: avoiding exposing localStorage to non-whitelisted social providers, r=mixedpuppy
☠☠ backed out by 8d3ce4697e65 ☠ ☠
authorGavin Sharp <gavin@gavinsharp.com>
Fri, 07 Jun 2013 18:08:50 -0700
changeset 146836 f370e521f004df60adb8265f44cff4536158da8c
parent 146835 61a8216f89917ac070ce0e18801ea5113a317247
child 146837 9d6db6508757e69cd182e9aefa75fd7dd4d39df9
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs879658
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 879658: avoiding exposing localStorage to non-whitelisted social providers, r=mixedpuppy
toolkit/components/social/FrameWorker.jsm
toolkit/components/social/SocialService.jsm
toolkit/components/social/test/browser/browser_frameworker.js
--- a/toolkit/components/social/FrameWorker.jsm
+++ b/toolkit/components/social/FrameWorker.jsm
@@ -25,26 +25,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"];
 
 var workerCache = {}; // keyed by URL.
 var _nextPortId = 1;
 
 // Retrieves a reference to a WorkerHandle associated with a FrameWorker and a
 // new ClientPort.
 this.getFrameWorkerHandle =
- function getFrameWorkerHandle(url, clientWindow, name, origin) {
+ function getFrameWorkerHandle(url, clientWindow, name, origin, exposeLocalStorage = false) {
   // first create the client port we are going to use.  Later we will
   // message the worker to create the worker port.
   let portid = _nextPortId++;
   let clientPort = new ClientPort(portid, clientWindow);
 
   let existingWorker = workerCache[url];
   if (!existingWorker) {
     // setup the worker and add this connection to the pending queue
-    let worker = new FrameWorker(url, name, origin);
+    let worker = new FrameWorker(url, name, origin, exposeLocalStorage);
     worker.pendingPorts.push(clientPort);
     existingWorker = workerCache[url] = worker;
   } else {
     // already have a worker - either queue or make the connection.
     if (existingWorker.loaded) {
       try {
         clientPort._createWorkerAndEntangle(existingWorker);
       }
@@ -64,25 +64,26 @@ this.getFrameWorkerHandle =
  * FrameWorker
  *
  * A FrameWorker is an iframe that is attached to the hiddenWindow,
  * which contains a pair of MessagePorts.  It is constructed with the
  * URL of some JavaScript that will be run in the context of the window;
  * the script does not have a full DOM but is instead run in a sandbox
  * that has a select set of methods cloned from the URL's domain.
  */
-function FrameWorker(url, name, origin) {
+function FrameWorker(url, name, origin, exposeLocalStorage) {
   this.url = url;
   this.name = name || url;
   this.ports = new Map();
   this.pendingPorts = [];
   this.loaded = false;
   this.reloading = false;
   this.origin = origin;
   this._injectController = null;
+  this.exposeLocalStorage = exposeLocalStorage;
 
   this.frame = makeHiddenFrame();
   this.load();
 }
 
 FrameWorker.prototype = {
   load: function FrameWorker_loadWorker() {
     this._injectController = function(doc, topic, data) {
@@ -128,21 +129,27 @@ FrameWorker.prototype = {
 
   createSandbox: function createSandbox() {
     let workerWindow = this.frame.contentWindow;
     let sandbox = new Cu.Sandbox(workerWindow);
 
     // copy the window apis onto the sandbox namespace only functions or
     // objects that are naturally a part of an iframe, I'm assuming they are
     // safe to import this way
-    let workerAPI = ['WebSocket', 'localStorage', 'atob', 'btoa',
+    let workerAPI = ['WebSocket', 'atob', 'btoa',
                      'clearInterval', 'clearTimeout', 'dump',
                      'setInterval', 'setTimeout', 'XMLHttpRequest',
                      'FileReader', 'Blob', 'EventSource', 'indexedDB',
                      'location'];
+
+    // Only expose localStorage if the caller opted-in
+    if (this.exposeLocalStorage) {
+      workerAPI.push('localStorage');
+    }
+
     // Bug 798660 - XHR and WebSocket have issues in a sandbox and need
     // to be unwrapped to work
     let needsWaive = ['XMLHttpRequest', 'WebSocket'];
     // Methods need to be bound with the proper |this|.
     let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval',
                      'setTimeout', 'clearTimeout'];
     workerAPI.forEach(function(fn) {
       try {
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -132,16 +132,52 @@ let SocialServiceInternal = {
         }
       });
     } finally {
       stmt.finalize();
     }
   }
 };
 
+XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
+  initService();
+  let providers = {};
+  for (let manifest of this.manifests) {
+    try {
+      if (ActiveProviders.has(manifest.origin)) {
+        let activationType = getOriginActivationType(manifest.origin);
+        let blessed = activationType == "builtin" ||
+                      activationType == "whitelist";
+        let provider = new SocialProvider(manifest, blessed);
+        providers[provider.origin] = provider;
+      }
+    } catch (err) {
+      Cu.reportError("SocialService: failed to load provider: " + manifest.origin +
+                     ", exception: " + err);
+    }
+  }
+  return providers;
+});
+
+function getOriginActivationType(origin) {
+  let prefname = SocialServiceInternal.getManifestPrefname(origin);
+  if (Services.prefs.getDefaultBranch("social.manifest.").getPrefType(prefname) == Services.prefs.PREF_STRING)
+    return 'builtin';
+
+  let whitelist = Services.prefs.getCharPref("social.whitelist").split(',');
+  if (whitelist.indexOf(origin) >= 0)
+    return 'whitelist';
+
+  let directories = Services.prefs.getCharPref("social.directories").split(',');
+  if (directories.indexOf(origin) >= 0)
+    return 'directory';
+
+  return 'foreign';
+}
+
 let ActiveProviders = {
   get _providers() {
     delete this._providers;
     this._providers = {};
     try {
       let pref = Services.prefs.getComplexValue("social.activeProviders",
                                                 Ci.nsISupportsString);
       this._providers = JSON.parse(pref);
@@ -299,33 +335,16 @@ function initService() {
     // enabled providers are not migrated.
     Cu.reportError("Error migrating social settings: " + e);
   }
   // Initialize the MozSocialAPI
   if (SocialServiceInternal.enabled)
     MozSocialAPI.enabled = true;
 }
 
-XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
-  initService();
-  let providers = {};
-  for (let manifest of this.manifests) {
-    try {
-      if (ActiveProviders.has(manifest.origin)) {
-        let provider = new SocialProvider(manifest);
-        providers[provider.origin] = provider;
-      }
-    } catch (err) {
-      Cu.reportError("SocialService: failed to load provider: " + manifest.origin +
-                     ", exception: " + err);
-    }
-  }
-  return providers;
-});
-
 function schedule(callback) {
   Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
 }
 
 // Public API
 this.SocialService = {
   get enabled() {
     return SocialServiceInternal.enabled;
@@ -439,32 +458,16 @@ this.SocialService = {
     });
   },
 
   // Returns an array of installed providers, sorted by frecency
   getOrderedProviderList: function(onDone) {
     SocialServiceInternal.orderedProviders(onDone);
   },
 
-  getOriginActivationType: function(origin) {
-    let prefname = SocialServiceInternal.getManifestPrefname(origin);
-    if (Services.prefs.getDefaultBranch("social.manifest.").getPrefType(prefname) == Services.prefs.PREF_STRING)
-      return 'builtin';
-
-    let whitelist = Services.prefs.getCharPref("social.whitelist").split(',');
-    if (whitelist.indexOf(origin) >= 0)
-      return 'whitelist';
-
-    let directories = Services.prefs.getCharPref("social.directories").split(',');
-    if (directories.indexOf(origin) >= 0)
-      return 'directory';
-
-    return 'foreign';
-  },
-
   _providerListeners: new Map(),
   registerProviderListener: function registerProviderListener(listener) {
     this._providerListeners.set(listener, 1);
   },
   unregisterProviderListener: function unregisterProviderListener(listener) {
     this._providerListeners.delete(listener);
   },
 
@@ -580,17 +583,17 @@ this.SocialService = {
       }.bind(this));
     }.bind(this));
   },
 
   _installProvider: function(aDOMDocument, data, installCallback) {
     let sourceURI = aDOMDocument.location.href;
     let installOrigin = aDOMDocument.nodePrincipal.origin;
 
-    let installType = this.getOriginActivationType(installOrigin);
+    let installType = getOriginActivationType(installOrigin);
     let manifest;
     if (data) {
       // if we get data, we MUST have a valid manifest generated from the data
       manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
       if (!manifest)
         throw new Error("SocialService.installProvider: service configuration is invalid from " + sourceURI);
     }
     let installer;
@@ -644,18 +647,19 @@ this.SocialService = {
 };
 
 /**
  * The SocialProvider object represents a social provider, and allows
  * access to its FrameWorker (if it has one).
  *
  * @constructor
  * @param {jsobj} object representing the manifest file describing this provider
+ * @param {bool} boolean indicating whether this provider is "built in"
  */
-function SocialProvider(input) {
+function SocialProvider(input, blessed = false) {
   if (!input.name)
     throw new Error("SocialProvider must be passed a name");
   if (!input.origin)
     throw new Error("SocialProvider must be passed an origin");
 
   let id = getAddonIDFromOrigin(input.origin);
   if (Services.blocklist.getAddonBlocklistState(id, input.version || "0") == Ci.nsIBlocklistService.STATE_BLOCKED)
     throw new Error("SocialProvider: provider with origin [" +
@@ -669,16 +673,17 @@ function SocialProvider(input) {
   this.sidebarURL = input.sidebarURL;
   this.shareURL = input.shareURL;
   this.origin = input.origin;
   let originUri = Services.io.newURI(input.origin, null, null);
   this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);
   this.ambientNotificationIcons = {};
   this.errorState = null;
   this.frecency = 0;
+  this.blessed = blessed;
   try {
     this.domain = etld.getBaseDomainFromHost(originUri.host);
   } catch(e) {
     this.domain = originUri.host;
   }
 }
 
 SocialProvider.prototype = {
@@ -864,18 +869,22 @@ SocialProvider.prototype = {
    *
    * Returns null if this provider has no workerURL, or is disabled.
    *
    * @param {DOMWindow} window (optional)
    */
   getWorkerPort: function getWorkerPort(window) {
     if (!this.workerURL || !this.enabled)
       return null;
-    return getFrameWorkerHandle(this.workerURL, window,
-                                "SocialProvider:" + this.origin, this.origin).port;
+    // Only allow localStorage in the frameworker for blessed providers
+    let allowLocalStorage = this.blessed;
+    let handle = getFrameWorkerHandle(this.workerURL, window,
+                                      "SocialProvider:" + this.origin, this.origin,
+                                      allowLocalStorage);
+    return handle.port;
   },
 
   /**
    * Checks if a given URI is of the same origin as the provider.
    *
    * Returns true or false.
    *
    * @param {URI or string} uri
--- a/toolkit/components/social/test/browser/browser_frameworker.js
+++ b/toolkit/components/social/test/browser/browser_frameworker.js
@@ -236,26 +236,50 @@ let tests = {
           ok = localStorage["foo"] == 1;
         } catch (e) {
           port.postMessage({topic: "done", result: "FAILED to read localStorage, " + e.toString() });
           return;
         }
         port.postMessage({topic: "done", result: "ok"});
       }
     }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testLocalStorage");
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testLocalStorage", null, true);
     worker.port.onmessage = function(e) {
       if (e.data.topic == "done") {
         is(e.data.result, "ok", "check the localStorage test worked");
         worker.terminate();
         cbnext();
       }
     }
   },
 
+  testNoLocalStorage: function(cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        try {
+          localStorage.setItem("foo", "1");
+        } catch(e) {
+          port.postMessage({topic: "done", result: "ok"});
+          return;
+        }
+
+        port.postMessage({topic: "done", result: "FAILED because localStorage was exposed" });
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testNoLocalStorage");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check that retrieving localStorage fails by default");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
   testBase64: function (cbnext) {
     let run = function() {
       onconnect = function(e) {
         let port = e.ports[0];
         var ok = false;
         try {
           ok = btoa("1234") == "MTIzNA==";
         } catch(e) {