Bug 985472 - cloneInto for sandbox. r=bholley, a=test-only
authorGabor Krizsanits <gkrizsanits@mozilla.com>
Tue, 25 Mar 2014 16:21:56 +0100
changeset 192193 4c244576343b
parent 192192 b955f950df88
child 192194 c66942faa3b2
push id3519
push userryanvm@gmail.com
push date2014-05-05 16:58 +0000
Treeherderresults
reviewersbholley, test-only
bugs985472
milestone30.0
Bug 985472 - cloneInto for sandbox. r=bholley, a=test-only
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/chrome/test_cloneInto.xul
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -617,16 +617,30 @@ CreateObjectIn(JSContext *cx, unsigned a
     }
 
     CreateObjectInOptions options(cx, optionsObj);
     if (calledWithOptions && !options.Parse())
         return false;
 
     return xpc::CreateObjectIn(cx, args[0], options, args.rval());
 }
+
+static bool
+CloneInto(JSContext *cx, unsigned argc, jsval *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (args.length() < 2) {
+        JS_ReportError(cx, "Function requires at least 2 arguments");
+        return false;
+    }
+
+    RootedValue options(cx, args.length() > 2 ? args[2] : UndefinedValue());
+    return xpc::CloneInto(cx, args[0], args[1], options, args.rval());
+}
+
 } /* namespace xpc */
 
 static bool
 sandbox_enumerate(JSContext *cx, HandleObject obj)
 {
     return JS_EnumerateStandardClasses(cx, obj);
 }
 
@@ -1126,16 +1140,17 @@ xpc::CreateSandboxObject(JSContext *cx, 
 
         if (!JS_DefineFunctions(cx, sandbox, SandboxFunctions))
             return NS_ERROR_XPC_UNEXPECTED;
 
         if (options.wantExportHelpers &&
             (!JS_DefineFunction(cx, sandbox, "exportFunction", ExportFunction, 3, 0) ||
              !JS_DefineFunction(cx, sandbox, "evalInWindow", EvalInWindow, 2, 0) ||
              !JS_DefineFunction(cx, sandbox, "createObjectIn", CreateObjectIn, 2, 0) ||
+             !JS_DefineFunction(cx, sandbox, "cloneInto", CloneInto, 3, 0) ||
              !JS_DefineFunction(cx, sandbox, "isProxy", IsProxy, 1, 0)))
             return NS_ERROR_XPC_UNEXPECTED;
 
         if (!options.globalProperties.Define(cx, sandbox))
             return NS_ERROR_XPC_UNEXPECTED;
     }
 
 
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -3613,50 +3613,58 @@ CloneIntoWriteStructuredClone(JSContext 
 // should only be used when the read and write are done together
 // synchronously.
 static JSStructuredCloneCallbacks CloneIntoCallbacks = {
     CloneIntoReadStructuredClone,
     CloneIntoWriteStructuredClone,
     nullptr
 };
 
+bool
+xpc::CloneInto(JSContext *aCx, HandleValue aValue, HandleValue aScope,
+               HandleValue aOptions, MutableHandleValue aCloned)
+{
+    if (!aScope.isObject())
+        return false;
+
+    RootedObject scope(aCx, &aScope.toObject());
+    scope = js::CheckedUnwrap(scope);
+    if(!scope) {
+        JS_ReportError(aCx, "Permission denied to clone object into scope");
+        return false;
+    }
+
+    if (!aOptions.isUndefined() && !aOptions.isObject()) {
+        JS_ReportError(aCx, "Invalid argument");
+        return false;
+    }
+
+    RootedObject optionsObject(aCx, aOptions.isObject() ? &aOptions.toObject()
+                                                        : nullptr);
+    CloneIntoOptions options(aCx, optionsObject);
+    if (aOptions.isObject() && !options.Parse())
+        return false;
+
+    {
+        CloneIntoCallbacksData data(aCx, &options);
+        JSAutoCompartment ac(aCx, scope);
+        if (!JS_StructuredClone(aCx, aValue, aCloned, &CloneIntoCallbacks, &data))
+            return false;
+    }
+
+    return JS_WrapValue(aCx, aCloned);
+}
+
 NS_IMETHODIMP
 nsXPCComponents_Utils::CloneInto(HandleValue aValue, HandleValue aScope,
                                  HandleValue aOptions, JSContext *aCx,
                                  MutableHandleValue aCloned)
 {
-    if (!aScope.isObject())
-        return NS_ERROR_INVALID_ARG;
-
-    RootedObject scope(aCx, &aScope.toObject());
-    scope = js::CheckedUnwrap(scope);
-    NS_ENSURE_TRUE(scope, NS_ERROR_FAILURE);
-
-    if (!aOptions.isUndefined() && !aOptions.isObject()) {
-        JS_ReportError(aCx, "Invalid argument");
-        return NS_ERROR_FAILURE;
-    }
-
-    RootedObject optionsObject(aCx, aOptions.isObject() ? &aOptions.toObject()
-                                                        : nullptr);
-    CloneIntoOptions options(aCx, optionsObject);
-    if (aOptions.isObject() && !options.Parse())
-        return NS_ERROR_FAILURE;
-
-    {
-        CloneIntoCallbacksData data(aCx, &options);
-        JSAutoCompartment ac(aCx, scope);
-        if (!JS_StructuredClone(aCx, aValue, aCloned, &CloneIntoCallbacks, &data))
-            return NS_ERROR_FAILURE;
-    }
-
-    if (!JS_WrapValue(aCx, aCloned))
-        return NS_ERROR_FAILURE;
-
-    return NS_OK;
+    return xpc::CloneInto(aCx, aValue, aScope, aOptions, aCloned) ?
+           NS_OK : NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP
 nsXPCComponents_Utils::GetWebIDLCallerPrincipal(nsIPrincipal **aResult)
 {
     // This API may only be when the Entry Settings Object corresponds to a
     // JS-implemented WebIDL call. In all other cases, the value will be null,
     // and we throw.
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3433,16 +3433,20 @@ CreateObjectIn(JSContext *cx, JS::Handle
 bool
 EvalInWindow(JSContext *cx, const nsAString &source, JS::HandleObject scope,
              JS::MutableHandleValue rval);
 
 bool
 ExportFunction(JSContext *cx, JS::HandleValue vscope, JS::HandleValue vfunction,
                JS::HandleValue voptions, JS::MutableHandleValue rval);
 
+bool
+CloneInto(JSContext *cx, JS::HandleValue vobj, JS::HandleValue vscope,
+          JS::HandleValue voptions, JS::MutableHandleValue rval);
+
 } /* namespace xpc */
 
 
 /***************************************************************************/
 // Inlined utilities.
 
 inline bool
 xpc_ForcePropertyResolve(JSContext* cx, JS::HandleObject obj, jsid id);
--- a/js/xpconnect/tests/chrome/test_cloneInto.xul
+++ b/js/xpconnect/tests/chrome/test_cloneInto.xul
@@ -98,43 +98,63 @@
       return;
     }
 
     if (type != 'null') {
       is (a.toSource(), b.toSource(), 'Matching using toSource()');
     }
   }
 
-  var sandbox = new Cu.Sandbox(window, { sandboxPrototype: window, wantXrays: true } );
+  var sandboxOptions = {
+    wantXrays: true,
+    wantExportHelpers: true,
+  };
+  var sandbox = new Cu.Sandbox(window, sandboxOptions);
+  // The second sandbox is for testing the exportHelper version of the cloneInto
+  var sandbox2 = new Cu.Sandbox("http://example.com", sandboxOptions);
+  sandbox.sandbox2 = sandbox2;
+
+  function cloneAndTest(test, cloneOptions) {
+    var output = sandbox.test = Cu.cloneInto(test, sandbox, cloneOptions);
+    compare(test, output);
+
+    sandbox.cloneOptions = cloneOptions;
+    output = Cu.evalInSandbox('cloneInto(test, sandbox2, cloneOptions)', sandbox);
+    compare(test, output);
+  }
 
   var tests = [
     1,
     null,
     true,
     'hello world',
     [1, 2, 3],
     { a: 1, b: 2 },
     { blob: new Blob([]), file: new File(new Blob([])) },
     new Date(),
     { a: 1, b: {}, c: [1, 2, 3, { d: new Blob([]) } ], e: 'hello world' },
   ];
 
   for (var i = 0; i < tests.length; ++i) {
-    var test = tests[i];
-
-    var output = Cu.cloneInto(test, sandbox);
-    compare(output, test);
+    cloneAndTest(tests[i]);
   }
 
   try {
     var output = Cu.cloneInto({ a: function() {} }, sandbox);
     ok(false, 'Function should not be cloned by default');
   } catch(e) {
     ok(true, 'Function should not be cloned by default');
   }
 
+  try {
+    sandbox2.sandbox = sandbox;
+    Cu.evalInSandbox('cloneInto({}, sandbox)', sandbox2);
+    ok(false, 'CloneInto should only work on less privileged target scopes.');
+  } catch(e) {
+    ok(/denied|insecure/.test(e),
+       'CloneInto should only work on less privileged target scopes.');
+  }
+
   var test = { a: function() { return 42; } }
-  var output = Cu.cloneInto(test, sandbox, { cloneFunctions: true });
-  compare(test, output);
-
+  cloneAndTest(test, { cloneFunctions: true });
   ]]>
   </script>
 </window>