Bug 1274708 Add BaseContext.jsonStringify() r?kmag draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 16 Jun 2016 08:30:58 -0700
changeset 379692 09726a61cd7da6d67cd223e2d48c3d1a865d6ec9
parent 378390 b9f4f38063951cd5a8b249911aea61869f40fd1f
child 379693 2d1f01f426a05229aa606eac54b012489fb89dd5
child 379718 742be2054363e5d353a485b3d8037d6a8c84f381
push id21023
push useraswan@mozilla.com
push dateThu, 16 Jun 2016 15:32:37 +0000
reviewerskmag
bugs1274708
milestone50.0a1
Bug 1274708 Add BaseContext.jsonStringify() r?kmag MozReview-Commit-ID: E4F1e8hDA5a
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -143,16 +143,17 @@ let gContextId = 0;
 class BaseContext {
   constructor(extensionId) {
     this.onClose = new Set();
     this.checkedLastError = false;
     this._lastError = null;
     this.contextId = ++gContextId;
     this.unloaded = false;
     this.extensionId = extensionId;
+    this.jsonSandbox = null;
   }
 
   get cloneScope() {
     throw new Error("Not implemented");
   }
 
   get principal() {
     throw new Error("Not implemented");
@@ -191,16 +192,34 @@ class BaseContext {
     try {
       ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
     } catch (e) {
       return false;
     }
     return true;
   }
 
+  /**
+   * Safely call JSON.stringify() on an object that comes from an
+   * extension.
+   *
+   * @param {array<any>} args Arguments for JSON.stringify()
+   * @returns {string} The stringified representation of obj
+   */
+  jsonStringify(...args) {
+    if (!this.jsonSandbox) {
+      this.jsonSandbox = Cu.Sandbox(this.principal, {
+        sameZoneAs: this.cloneScope,
+        wantXrays: false,
+      });
+    }
+
+    return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
+  }
+
   callOnClose(obj) {
     this.onClose.add(obj);
   }
 
   forgetOnClose(obj) {
     this.onClose.delete(obj);
   }
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -9,17 +9,17 @@ var {
   BaseContext,
   EventManager,
   SingletonEventManager,
 } = ExtensionUtils;
 
 class StubContext extends BaseContext {
   constructor() {
     super();
-    this.sandbox = new Cu.Sandbox(global);
+    this.sandbox = Cu.Sandbox(global);
   }
 
   get cloneScope() {
     return this. sandbox;
   }
 
   get extension() {
     return {id: "test@web.extension"};
@@ -122,8 +122,67 @@ add_task(function* test_post_unload_list
 
   context.unload();
 
   // The `setTimeout` ensures that we return to the event loop after
   // promise resolution, which means we're guaranteed to return after
   // any micro-tasks that get enqueued by the resolution handlers above.
   yield new Promise(resolve => setTimeout(resolve, 0));
 });
+
+class Context extends BaseContext {
+  constructor(principal) {
+    super();
+    Object.defineProperty(this, "principal", {
+      value: principal,
+      configurable: true,
+    });
+    this.sandbox = Cu.Sandbox(principal, {wantXrays: false});
+    this.extension = {id: "test@web.extension"};
+  }
+
+  get cloneScope() {
+    return this.sandbox;
+  }
+}
+
+let ssm = Services.scriptSecurityManager;
+const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
+const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org");
+
+// Test that toJSON() works in the json sandbox
+add_task(function* test_stringify_toJSON() {
+  let context = new Context(PRINCIPAL1);
+  let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox);
+
+  let stringified = context.jsonStringify(obj);
+  let expected = JSON.stringify({visible: true});
+  equal(stringified, expected, "Stringified object with toJSON() method is as expected");
+});
+
+// Test that stringifying in inaccessible property throws
+add_task(function* test_stringify_inaccessible() {
+  let context = new Context(PRINCIPAL1);
+  let sandbox = context.sandbox;
+  let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+  Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+  let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+  Assert.throws(() => {
+    context.jsonStringify(obj);
+  });
+});
+
+add_task(function* test_stringify_accessible() {
+  // Test that an accessible property from another global is included
+  let principal = ssm.createExpandedPrincipal([PRINCIPAL1, PRINCIPAL2]);
+  let context = new Context(principal);
+  let sandbox = context.sandbox;
+  let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+  Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+  let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+  let stringified = context.jsonStringify(obj);
+
+  let expected = JSON.stringify({local: true, nested: {subobject: true}});
+  equal(stringified, expected, "Stringified object with accessible property is as expected");
+});
+