[STRAW MAN] Bug 1208775: [webext] Make `this` and `window` consistent in content script scopes. draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 05 Feb 2016 14:55:19 -0800
changeset 329300 352f0e5f0803b24da6f1d7ccdaf50d88befd29b3
parent 329092 75c21c179b63e364b42b123e24f12417c4f14539
child 513936 7436d4d63672ed0564641c12502eca881f8ca5a2
push id10499
push usermaglione.k@gmail.com
push dateFri, 05 Feb 2016 22:57:37 +0000
bugs1208775
milestone47.0a1
[STRAW MAN] Bug 1208775: [webext] Make `this` and `window` consistent in content script scopes.
js/xpconnect/idl/mozIJSSubScriptLoader.idl
js/xpconnect/loader/mozJSSubScriptLoader.cpp
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_contentscript_scope.html
--- a/js/xpconnect/idl/mozIJSSubScriptLoader.idl
+++ b/js/xpconnect/idl/mozIJSSubScriptLoader.idl
@@ -34,16 +34,21 @@ interface mozIJSSubScriptLoader : nsISup
      * In JS, the signature looks like:
      * rv = loadSubScript (url, optionsObject)
      * @param url the url of the sub-script, it MUST be either a file:,
      *            resource:, or chrome: url, and MUST be local.
      * @param optionsObject an object with parameters. Valid parameters are:
      *                      - charset: specifying the character encoding of the file (default: ASCII)
      *                      - target:  an object to evaluate onto (default: global object of the caller)
      *                      - ignoreCache: if set to true, will bypass the cache for reading the file.
+     *                      - targetGlobal: if set, code is evaluated in the context of the given global,
+     *                                      rather than in the scope of the unwrapped target object. If
+     *                                      the target object does not already belong to that global, it
+     *                                      will be wrapped in the appropriate security wrappers for the
+     *                                      target.
      * @retval rv the value returned by the sub-script
      */
     [implicit_jscontext]
     jsval loadSubScriptWithOptions(in AString url, in jsval options);
 
     /*
      * Compiles a JS script off the main thread and calls back the
      * observer once it's done.
--- a/js/xpconnect/loader/mozJSSubScriptLoader.cpp
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -42,29 +42,32 @@ using namespace mozilla;
 using namespace mozilla::dom;
 
 class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase {
 public:
     explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(),
                                   JSObject* options = nullptr)
         : OptionsBase(cx, options)
         , target(cx)
+        , targetGlobal(cx)
         , charset(NullString())
         , ignoreCache(false)
         , async(false)
     { }
 
     virtual bool Parse() {
       return ParseObject("target", &target) &&
+             ParseObject("targetGlobal", &targetGlobal) &&
              ParseString("charset", charset) &&
              ParseBoolean("ignoreCache", &ignoreCache) &&
              ParseBoolean("async", &async);
     }
 
     RootedObject target;
+    RootedObject targetGlobal;
     nsString charset;
     bool ignoreCache;
     bool async;
 };
 
 
 /* load() error msgs, XXX localize? */
 #define LOAD_ERROR_NOSERVICE "Error creating IO Service."
@@ -579,21 +582,32 @@ mozJSSubScriptLoader::DoLoadSubScriptWit
 
     if (options.target)
         targetObj = options.target;
 
     // Remember an object out of the calling compartment so that we
     // can properly wrap the result later.
     nsCOMPtr<nsIPrincipal> principal = mSystemPrincipal;
     RootedObject result_obj(cx, targetObj);
-    targetObj = JS_FindCompilationScope(cx, targetObj);
-    if (!targetObj)
-        return NS_ERROR_FAILURE;
+
+    if (options.targetGlobal) {
+        RootedObject scope(cx, JS_FindCompilationScope(cx, options.targetGlobal));
+        JSAutoCompartment ac(cx, scope);
 
-    if (targetObj != result_obj)
+        if (!JS_WrapObject(cx, &targetObj))
+            return NS_ERROR_FAILURE;
+    } else {
+        // If we're passed a cross-compartment wrapper or window proxy, and we're asked to unwrap
+        // it, do so, and evaluate the script in the unwrapped target scope.
+        targetObj = JS_FindCompilationScope(cx, targetObj);
+        if (!targetObj)
+            return NS_ERROR_FAILURE;
+    }
+
+    if (options.targetGlobal || targetObj != result_obj)
         principal = GetObjectPrincipal(targetObj);
 
     JSAutoCompartment ac(cx, targetObj);
 
     /* load up the url.  From here on, failures are reflected as ``custom''
      * js exceptions */
     nsCOMPtr<nsIURI> uri;
     nsAutoCString uriStr;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -187,19 +187,20 @@ Script.prototype = {
         // `document_idle` state.
         if (AppConstants.platform == "gonk" && scheduled != "document_idle") {
           Cu.reportError(`Script injection: ignoring ${url} at ${scheduled}`);
           continue;
         }
         url = extension.baseURI.resolve(url);
 
         let options = {
-          target: sandbox,
+          target: sandbox.window,
           charset: "UTF-8",
           async: AppConstants.platform == "gonk",
+          targetGlobal: sandbox,
         };
         try {
           result = Services.scriptloader.loadSubScriptWithOptions(url, options);
         } catch (e) {
           Cu.reportError(e);
           this.deferred.reject(e.message);
         }
       }
@@ -286,16 +287,21 @@ class ExtensionContext extends BaseConte
       });
     } else {
       this.sandbox = Cu.Sandbox(prin, {
         sandboxPrototype: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
         wantGlobalProperties: ["XMLHttpRequest"],
       });
+
+      // Copy the sandbox's semi-privileged XMLHttpRequest constructor
+      // to the window security wrapper, which is the execution context
+      // for content scripts.
+      Cu.evalInSandbox(`window.XMLHttpRequest = XMLHttpRequest;`, this.sandbox);
     }
 
     let delegate = {
       getSender(context, target, sender) {
         // Nothing to do here.
       },
     };
 
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -23,16 +23,17 @@ support-files =
   file_ext_test_api_injection.js
   file_permission_xhr.html
 
 [test_ext_simple.html]
 [test_ext_schema.html]
 [test_ext_geturl.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
+[test_ext_contentscript_scope.html]
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_api_injection.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_localStorage.html]
 [test_ext_onmessage_removelistener.html]
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_scope.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for content script scopes</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_scope() {
+  function background() {
+    browser.runtime.onMessage.addListener(([msg, ...args]) => {
+      if (msg == "content-script-complete") {
+        browser.test.notifyPass(msg);
+      } else if (msg == "assert-true") {
+        browser.test.assertTrue(...args);
+      } else if (msg == "assert-eq") {
+        browser.test.assertEq(...args);
+      } else {
+        browser.test.fail(`Unexpected message: ${msg}`);
+      }
+    });
+  }
+
+  function contentScript() {
+    /* eslint-disable mozilla/var-only-at-top-level */
+
+    function assertTrue(val, msg) {
+      browser.runtime.sendMessage(["assert-true", Boolean(val), msg]);
+    }
+    function assertEq(...args) {
+      browser.runtime.sendMessage(["assert-eq", ...args]);
+    }
+
+    assertTrue(!("chrome" in window), "No window.chrome property");
+    assertTrue(!("browser" in window), "No window.browser property");
+
+    assertTrue(!("chrome" in this), "No this.chrome property");
+    assertTrue(!("browser" in this), "No this.browser property");
+
+    assertTrue(this == window, "this == window");
+    assertTrue(this === window, "this === window");
+
+    var foo = "bar";
+    assertTrue("foo" in this, "this.foo is present");
+    assertTrue("foo" in window, "window.foo is present");
+
+    assertEq("bar", foo, "foo is correct");
+    assertEq("bar", this.foo, "this.foo is correct");
+    assertEq("bar", window.foo, "window.foo is correct");
+
+    for (let varName of ["foo", "chrome", "browser"]) {
+      assertTrue(window.wrappedJSObject.eval(`!("${varName}" in window)`),
+                 `No window.${varName} property in unprivileged context`);
+    }
+
+    chrome.runtime.sendMessage(["content-script-complete"]);
+
+    /* eslint-enable mozilla/var-only-at-top-level */
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [
+        {
+          "matches": ["http://mochi.test/*/file_sample.html"],
+          "js": ["content_script.js"],
+          "run_at": "document_idle",
+        },
+      ],
+    },
+    background: `(${background})()`,
+
+    files: {
+      // We need to have a raw script rather than a self-invoking function,
+      // here, for various tests to be valid.
+      "content_script.js": String(contentScript).replace(/^.*?\{([^]*)\}$/, "$1"),
+    },
+  });
+
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+
+  yield extension.awaitFinish("content-script-complete");
+
+  win.close();
+
+  yield extension.unload();
+});
+</script>
+
+</body>
+</html>