Bug 658909 - Implement carefully-checked unwrapping in XPCCallContext. r=mrbkap
authorBobby Holley <bobbyholley@gmail.com>
Sat, 16 Mar 2013 22:58:14 -0700
changeset 125054 d4253db9e56065538c084b95ab87405172af85a6
parent 125053 cd8e50acc2fe2159743983110da60fd11a339a5b
child 125055 682ad22f239f7771fdb5f9fe713d8a99c960a158
push id24762
push userbobbyholley@gmail.com
push dateSun, 17 Mar 2013 05:58:40 +0000
treeherdermozilla-inbound@3a5f73a4f816 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs658909
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 658909 - Implement carefully-checked unwrapping in XPCCallContext. r=mrbkap
js/xpconnect/src/XPCCallContext.cpp
js/xpconnect/src/XPCWrappedNativeJSOps.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/mochitest/file_bug802557.html
js/xpconnect/tests/mochitest/test_bug802557.html
js/xpconnect/wrappers/WrapperFactory.h
--- a/js/xpconnect/src/XPCCallContext.cpp
+++ b/js/xpconnect/src/XPCCallContext.cpp
@@ -2,20 +2,22 @@
  *
  * 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/. */
 
 /* Call context. */
 
 #include "mozilla/Util.h"
+#include "AccessCheck.h"
 
 #include "xpcprivate.h"
 
 using namespace mozilla;
+using namespace xpc;
 
 XPCCallContext::XPCCallContext(XPCContext::LangType callerLanguage,
                                JSContext* cx    /* = nullptr    */,
                                JSObject* obj    /* = nullptr    */,
                                JSObject* funobj /* = nullptr    */,
                                jsid name        /* = JSID_VOID */,
                                unsigned argc       /* = NO_ARGS   */,
                                jsval *argv      /* = nullptr    */,
@@ -137,20 +139,32 @@ XPCCallContext::Init(XPCContext::LangTyp
     mState = HAVE_SCOPE;
 
     mMethodIndex = 0xDEAD;
 
     mState = HAVE_OBJECT;
 
     mTearOff = nullptr;
     if (wrapperInitOptions == INIT_SHOULD_LOOKUP_WRAPPER) {
-        mWrapper = XPCWrappedNative::GetWrappedNativeOfJSObject(mJSContext, obj,
-                                                                funobj,
-                                                                &mFlattenedJSObject,
-                                                                &mTearOff);
+
+        // If the object is a security wrapper, GetWrappedNativeOfJSObject can't
+        // handle it. Do special handling here to make cross-origin Xrays work.
+        if (WrapperFactory::IsSecurityWrapper(obj)) {
+            mWrapper = UnwrapThisIfAllowed(obj, funobj, argc);
+            if (!mWrapper) {
+                JS_ReportError(mJSContext, "Permission denied to call method on |this|");
+                mState = INIT_FAILED;
+                return;
+            }
+        } else {
+            mWrapper = XPCWrappedNative::GetWrappedNativeOfJSObject(mJSContext,
+                                                                    obj, funobj,
+                                                                    &mFlattenedJSObject,
+                                                                    &mTearOff);
+        }
         if (mWrapper) {
             mFlattenedJSObject = mWrapper->GetFlatJSObject();
 
             if (mTearOff)
                 mScriptableInfo = nullptr;
             else
                 mScriptableInfo = mWrapper->GetScriptableInfo();
         } else {
@@ -426,8 +440,63 @@ void
 XPCLazyCallContext::AssertContextIsTopOfStack(JSContext* cx)
 {
     XPCJSContextStack* stack = XPCJSRuntime::Get()->GetJSContextStack();
 
     JSContext *topJSContext = stack->Peek();
     NS_ASSERTION(cx == topJSContext, "wrong context on XPCJSContextStack!");
 }
 #endif
+
+XPCWrappedNative*
+XPCCallContext::UnwrapThisIfAllowed(JSObject *obj, JSObject *fun, unsigned argc)
+{
+    // We should only get here for objects that aren't safe to unwrap.
+    MOZ_ASSERT(!js::UnwrapObjectChecked(obj));
+    MOZ_ASSERT(js::IsObjectInContextCompartment(obj, mJSContext));
+
+    // Determine if we're allowed to unwrap the security wrapper to invoke the
+    // method.
+    //
+    // We have the Interface and Member that this corresponds to, but
+    // unfortunately our access checks are based on the object class name and
+    // property name. So we cheat a little bit here - we verify that the object
+    // does indeed implement the method's Interface, and then just check that we
+    // can successfully access property with method's name from the object.
+
+    // First, get the XPCWN out of the underlying object. We should have a wrapper
+    // here, potentially an outer window proxy, and then an XPCWN.
+    MOZ_ASSERT(js::IsWrapper(obj));
+    JSObject *unwrapped = js::UnwrapObject(obj, /* stopAtOuter = */ false);
+    MOZ_ASSERT(unwrapped == JS_ObjectToInnerObject(mJSContext, js::Wrapper::wrappedObject(obj)));
+
+    // Make sure we have an XPCWN, and grab it.
+    MOZ_ASSERT(!IS_SLIM_WRAPPER(unwrapped), "security wrapping morphs slim wrappers");
+    if (!IS_WRAPPER_CLASS(js::GetObjectClass(unwrapped)))
+        return nullptr;
+    XPCWrappedNative *wn = (XPCWrappedNative*)js::GetObjectPrivate(unwrapped);
+
+    // Next, get the call info off the function object.
+    XPCNativeInterface *interface;
+    XPCNativeMember *member;
+    XPCNativeMember::GetCallInfo(fun, &interface, &member);
+
+    // To be extra safe, make sure that the underlying native implements the
+    // interface before unwrapping. Even if we didn't check this, we'd still
+    // theoretically fail during tearoff lookup for mismatched methods.
+    if (!wn->HasInterfaceNoQI(*interface->GetIID()))
+        return nullptr;
+
+    // See if the access is permitted.
+    //
+    // NB: This calculation of SET vs GET is a bit wonky, but that's what
+    // XPC_WN_GetterSetter does.
+    bool set = argc && argc != NO_ARGS && member->IsWritableAttribute();
+    js::Wrapper::Action act = set ? js::Wrapper::SET : js::Wrapper::GET;
+    js::Wrapper *handler = js::Wrapper::wrapperHandler(obj);
+    bool ignored;
+    if (!handler->enter(mJSContext, obj, member->GetName(), act, &ignored))
+        return nullptr;
+
+    // Ok, this call is safe.
+    return wn;
+}
+
--- a/js/xpconnect/src/XPCWrappedNativeJSOps.cpp
+++ b/js/xpconnect/src/XPCWrappedNativeJSOps.cpp
@@ -4,16 +4,17 @@
  * 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/. */
 
 /* JavaScript JSClasses and JSOps for our Wrapped Native JS Objects. */
 
 #include "xpcprivate.h"
 #include "XPCWrapper.h"
+#include "AccessCheck.h"
 #include "nsWrapperCacheInlines.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/Preferences.h"
 
 using namespace mozilla;
 /***************************************************************************/
 
 // All of the exceptions thrown into JS from this file go through here.
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -1243,16 +1243,19 @@ private:
               JSObject* obj,
               JSObject* funobj,
               WrapperInitOptions wrapperInitOptions,
               jsid name,
               unsigned argc,
               jsval *argv,
               jsval *rval);
 
+    XPCWrappedNative* UnwrapThisIfAllowed(JSObject *obj, JSObject *fun,
+                                          unsigned argc);
+
 private:
     // posible values for mState
     enum State {
         INIT_FAILED,
         SYSTEM_SHUTDOWN,
         HAVE_CONTEXT,
         HAVE_SCOPE,
         HAVE_OBJECT,
--- a/js/xpconnect/tests/mochitest/file_bug802557.html
+++ b/js/xpconnect/tests/mochitest/file_bug802557.html
@@ -50,25 +50,17 @@ function mergeObjects(a, b) {
   Object.getOwnPropertyNames(a).forEach(function(name) rv[name] = a[name]);
   Object.getOwnPropertyNames(b).forEach(function(name) rv[name] = b[name]);
   return rv;
 }
 
 function getAllTests() {
   var innerTests = getTests(false);
   var outerTests = getTests(true);
-  outerTests.getLocationApply1.skipMessageCheck = true;
-  outerTests.getLocationApply2.skipMessageCheck = true;
-  outerTests.getLocationApply3.skipMessageCheck = true;
-  outerTests.getLocationViaPrototype.skipMessageCheck = true;
-  outerTests.getHrefViaApply.skipMessageCheck = true;
-  outerTests.getHrefViaPrototype.skipMessageCheck = true;
   return mergeObjects(mungeNames(innerTests, '_inner'),
                       mungeNames(outerTests, '_outer'));
 }
 
-
-
 </script>
 </head>
 <body>
 </body>
 </html>
--- a/js/xpconnect/tests/mochitest/test_bug802557.html
+++ b/js/xpconnect/tests/mochitest/test_bug802557.html
@@ -8,27 +8,23 @@ https://bugzilla.mozilla.org/show_bug.cg
   <title>Test for Bug 802557</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script type="application/javascript">
 
   /** Test for Bug 802557 **/
   SimpleTest.waitForExplicitFinish();
 
-  // In the case of apply()ing on a Location object that's script access only,
-  // we just throw a very bland XPConnect message. In other cases though, we want
-  // to check the message to make sure we're throwing a security exception and not
-  // something else.
-  function checkThrows(fun, desc, skipMessageCheck) {
+  function checkThrows(fun, desc) {
     try {
       fun();
       ok(false, "Didn't throw when " + desc);
     } catch(e) {
       ok(true, "Threw when " + desc + " " + e);
-      (skipMessageCheck ? todo : ok)(/denied|insecure/.exec(e), "Should be security exception");
+      ok(/denied|insecure/.exec(e), "Should be security exception");
     }
   }
 
   var loadCount = 0;
   function go() {
     ++loadCount;
     window.ifr = document.getElementById('ifr');
     window.iWin = ifr.contentWindow;
@@ -54,32 +50,31 @@ https://bugzilla.mozilla.org/show_bug.cg
           ok(false, "Threw while applying " + item[1] + " to same-origin location object: " + e);
         }
       });
       ifr.src = "http://example.org/tests/js/xpconnect/tests/mochitest/file_empty.html";
     }
     else if (loadCount == 2) {
       gGetters.forEach(function(item) {
         checkThrows(function() { item[0].call(gLoc); },
-                    'call()ing ' + item[1] + ' after navigation cross-origin',
-                    /* skipMessageCheck = */ false);
+                    'call()ing ' + item[1] + ' after navigation cross-origin');
       });
       ifr.src = 'http://mochi.test:8888/tests/js/xpconnect/tests/mochitest/file_bug802557.html';
     }
     else if (loadCount == 3) {
       gTestFunctions = ifr.contentWindow.getAllTests();
       var win = ifr.contentWindow;
       for (fun in gTestFunctions)
          is(gTestFunctions[fun](), win.location.toString(), "allowed via " + fun);
       win.location = 'http://example.org/tests/js/xpconnect/tests/mochitest/file_bug802557.html';
     }
     else if (loadCount == 4) {
       for (fun in gTestFunctions) {
         var f = gTestFunctions[fun];
-        checkThrows(f, "calling " + fun, f.skipMessageCheck);
+        checkThrows(f, "calling " + fun);
       }
       SimpleTest.finish();
     }
   }
 
 
 
 </script>
--- a/js/xpconnect/wrappers/WrapperFactory.h
+++ b/js/xpconnect/wrappers/WrapperFactory.h
@@ -30,16 +30,20 @@ class WrapperFactory {
     static bool IsXrayWrapper(JSObject *wrapper) {
         return HasWrapperFlag(wrapper, IS_XRAY_WRAPPER_FLAG);
     }
 
     static bool HasWaiveXrayFlag(JSObject *wrapper) {
         return HasWrapperFlag(wrapper, WAIVE_XRAY_WRAPPER_FLAG);
     }
 
+    static bool IsSecurityWrapper(JSObject *obj) {
+        return !js::UnwrapObjectChecked(obj);
+    }
+
     static JSObject *GetXrayWaiver(JSObject *obj);
     static JSObject *CreateXrayWaiver(JSContext *cx, JSObject *obj);
     static JSObject *WaiveXray(JSContext *cx, JSObject *obj);
 
     static JSObject *DoubleWrap(JSContext *cx, JSObject *obj, unsigned flags);
 
     // Prepare a given object for wrapping in a new compartment.
     static JSObject *PrepareForWrapping(JSContext *cx,