Bug 821809 - Part3: nsEp for content-script. r=rFobic a=abillings
authorGabor Krizsanits <gkrizsanits@mozilla.com>
Thu, 10 Apr 2014 19:52:54 +0200
changeset 192785 2917948f72144e98271dd7124cdab31de1ab9f58
parent 192784 902765e9c65a81be34361d11eb64c6e5b53eddb8
child 192786 c274c9a5a39808ed5f3de05a8e6562af43053a66
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrFobic, abillings
bugs821809
milestone30.0a2
Bug 821809 - Part3: nsEp for content-script. r=rFobic a=abillings
addon-sdk/source/lib/sdk/content/sandbox.js
addon-sdk/source/test/addons/unsafe-content-script/main.js
addon-sdk/source/test/addons/unsafe-content-script/package.json
addon-sdk/source/test/test-content-script.js
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -33,16 +33,22 @@ const metadata = require('@loader/option
 
 // Fetch additional list of domains to authorize access to for each content
 // script. It is stored in manifest `metadata` field which contains
 // package.json data. This list is originaly defined by authors in
 // `permissions` attribute of their package.json addon file.
 const permissions = (metadata && metadata['permissions']) || {};
 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
 
+const waiveSecurityMembrane = !!permissions['unsafe-content-script'];
+
+const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager;
+const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].
+  getService(Ci.nsIScriptSecurityManager);
+
 const JS_VERSION = '1.8';
 
 const WorkerSandbox = Class({
   implements: [ EventTarget ],
 
   /**
    * Emit a message to the worker content sandbox
    */
@@ -91,49 +97,57 @@ const WorkerSandbox = Class({
     // We receive a wrapped window, that may be an xraywrapper if it's content
     let proto = window;
 
     // TODO necessary?
     // Ensure that `emit` has always the right `this`
     this.emit = this.emit.bind(this);
     this.emitSync = this.emitSync.bind(this);
 
-    // Eventually use expanded principal sandbox feature, if some are given.
-    //
+    // Use expanded principal for content-script if the content is a
+    // regular web content for better isolation.
+    // (This behavior can be turned off for now with the unsafe-content-script
+    // flag to give addon developers time for making the necessary changes)
     // But prevent it when the Worker isn't used for a content script but for
     // injecting `addon` object into a Panel, Widget, ... scope.
     // That's because:
     // 1/ It is useless to use multiple domains as the worker is only used
     // to communicate with the addon,
     // 2/ By using it it would prevent the document to have access to any JS
     // value of the worker. As JS values coming from multiple domain principals
     // can't be accessed by 'mono-principals' (principal with only one domain).
     // Even if this principal is for a domain that is specified in the multiple
     // domain principal.
     let principals = window;
     let wantGlobalProperties = [];
-    if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) {
-      principals = EXPANDED_PRINCIPALS.concat(window);
-      // We have to replace XHR constructor of the content document
-      // with a custom cross origin one, automagically added by platform code:
-      delete proto.XMLHttpRequest;
-      wantGlobalProperties.push('XMLHttpRequest');
+    let isSystemPrincipal = secMan.isSystemPrincipal(
+      window.document.nodePrincipal);
+    if (!isSystemPrincipal && !requiresAddonGlobal(worker)) {
+      if (EXPANDED_PRINCIPALS.length > 0) {
+        // We have to replace XHR constructor of the content document
+        // with a custom cross origin one, automagically added by platform code:
+        delete proto.XMLHttpRequest;
+        wantGlobalProperties.push('XMLHttpRequest');
+      }
+      if (!waiveSecurityMembrane)
+        principals = EXPANDED_PRINCIPALS.concat(window);
     }
 
     // Instantiate trusted code in another Sandbox in order to prevent content
     // script from messing with standard classes used by proxy and API code.
     let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
     apiSandbox.console = console;
 
     // Create the sandbox and bind it to window in order for content scripts to
     // have access to all standard globals (window, document, ...)
     let content = sandbox(principals, {
       sandboxPrototype: proto,
       wantXrays: true,
       wantGlobalProperties: wantGlobalProperties,
+      wantExportHelpers: !waiveSecurityMembrane,
       sameZoneAs: window,
       metadata: {
         SDKContentScript: true,
         'inner-window-id': getInnerId(window)
       }
     });
     model.sandbox = content;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/unsafe-content-script/main.js
@@ -0,0 +1,61 @@
+/* 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/. */
+"use strict";
+
+const { create: makeFrame } = require("sdk/frame/utils");
+const { window } = require("sdk/addon/window");
+const { Loader } = require('sdk/test/loader');
+const loader = Loader(module);
+const Worker = loader.require("sdk/content/worker").Worker;
+
+exports.testMembranelessMode = function(assert, done) {
+
+  let url = "data:text/html;charset=utf-8," + encodeURIComponent(
+    '<script>' +
+    'function runTest() {' +
+    '  assert(fuu.bar == 42, "Content-script objects should be accessible to content with' +
+    '         the unsafe-content-script flag on.");' +
+    '}' +
+    '</script>'
+  );
+
+  let element = makeFrame(window.document, {
+    nodeName: "iframe",
+    type: "content",
+    allowJavascript: true,
+    allowPlugins: true,
+    allowAuth: true,
+    uri: url
+  });
+
+  element.addEventListener("DOMContentLoaded", onDOMReady, false);
+
+  function onDOMReady() {
+    let worker = Worker({
+      window: element.contentWindow,
+      contentScript:
+        'new ' + function () {
+          var assert = function assert(v, msg) {
+            self.port.emit("assert", { assertion: v, msg: msg });
+          }
+          var done = function done() {
+            self.port.emit("done");
+          }
+          window.wrappedJSObject.fuu = { bar: 42 };
+          window.wrappedJSObject.assert = assert;
+          window.wrappedJSObject.runTest();
+          done();
+        }
+    });
+    worker.port.on("done", function () {
+      element.parentNode.removeChild(element);
+      done();
+    });
+    worker.port.on("assert", function (data) {
+      assert.ok(data.assertion, data.msg);
+    });
+  }
+};
+
+require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/unsafe-content-script/package.json
@@ -0,0 +1,6 @@
+{
+  "id": "content-permissions",
+  "permissions": {
+    "unsafe-content-script": true
+  }
+}
--- a/addon-sdk/source/test/test-content-script.js
+++ b/addon-sdk/source/test/test-content-script.js
@@ -841,9 +841,35 @@ exports["test MutationObvserver"] = crea
 
       // Modify the DOM
       link.setAttribute("href", "bar");
     }
   );
 
 });
 
+let html = '<script>' +
+  'var accessCheck = function() {' +
+  '  assert(true, "exporting function works");' +
+  '  try{' +
+  '    exportedObj.prop;' +
+  '    assert(false, "content should not have access to content-script");' +
+  '  } catch(e) {' +
+  '    assert(e.toString().indexOf("Permission denied") != -1,' +
+  '           "content should not have access to content-script");' +
+  '  }' +
+  '}</script>';
+exports["test nsEp for content-script"] = createProxyTest(html, function (helper) {
+
+  helper.createWorker(
+    'let glob = this; new ' + function ContentScriptScope() {
+
+      exportFunction(assert, unsafeWindow, { defineAs: "assert" });
+      window.wrappedJSObject.assert(true, "assert exported");
+      window.wrappedJSObject.exportedObj = { prop: 42 };
+      window.wrappedJSObject.accessCheck();
+      done();
+    }
+  );
+
+});
+
 require("test").run(exports);