Bug 879658 - Don't expose localStorage to FrameWorker for non-whitelisted social providers. r=mixedpuppy, a=lsblakk
authorGavin Sharp <gavin@gavinsharp.com>
Mon, 17 Jun 2013 17:25:06 -0400
changeset 142949 104a6f247a404d5bf493807fe1416aab1c5d027e
parent 142948 c569195c2993d81be6aceb6be81bbcc360094a86
child 142950 6014e97def09d25042f5953171efff22d54502f4
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, lsblakk
bugs879658
milestone23.0a2
Bug 879658 - Don't expose localStorage to FrameWorker for non-whitelisted social providers. r=mixedpuppy, a=lsblakk
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
@@ -74,16 +74,52 @@ let SocialServiceInternal = {
                        ", exception: " + err);
       }
     }
     let originUri = Services.io.newURI(origin, null, null);
     return originUri.hostPort.replace('.','-');
   }
 };
 
+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);
@@ -241,33 +277,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;
@@ -378,30 +397,18 @@ this.SocialService = {
 
   // Returns an array of installed providers.
   getProviderList: function getProviderList(onDone) {
     schedule(function () {
       onDone(SocialServiceInternal.providerArray);
     });
   },
 
-  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';
+  getOriginActivationType: function (origin) {
+    return getOriginActivationType(origin);
   },
 
   _providerListeners: new Map(),
   registerProviderListener: function registerProviderListener(listener) {
     this._providerListeners.set(listener, 1);
   },
   unregisterProviderListener: function unregisterProviderListener(listener) {
     this._providerListeners.delete(listener);
@@ -519,17 +526,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;
@@ -583,18 +590,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 [" +
@@ -607,16 +615,17 @@ function SocialProvider(input) {
   this.workerURL = input.workerURL;
   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.blessed = blessed;
 }
 
 SocialProvider.prototype = {
   // Provider enabled/disabled state. Disabled providers do not have active
   // connections to their FrameWorkers.
   _enabled: false,
   get enabled() {
     return this._enabled;
@@ -797,18 +806,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) {