From 8168b8f791dfac988e9f8eee87fe8d82e3857f51 Mon Sep 17 00:00:00 2001 From: Blake Kaplan Date: Wed, 30 Sep 2009 19:38:57 -0700 Subject: [PATCH] Bug 518991 - Implement access controls in COWs. For now, this is opt-in default unsafe. r=jst --- .../xpconnect/src/XPCChromeObjectWrapper.cpp | 213 +++++++++++++++++- js/src/xpconnect/src/xpcinlines.h | 10 +- js/src/xpconnect/src/xpcjsruntime.cpp | 3 +- js/src/xpconnect/src/xpcprivate.h | 4 + 4 files changed, 216 insertions(+), 14 deletions(-) diff --git a/js/src/xpconnect/src/XPCChromeObjectWrapper.cpp b/js/src/xpconnect/src/XPCChromeObjectWrapper.cpp index e4e188e79c6c..92566e6a4d0a 100644 --- a/js/src/xpconnect/src/XPCChromeObjectWrapper.cpp +++ b/js/src/xpconnect/src/XPCChromeObjectWrapper.cpp @@ -48,6 +48,167 @@ // This file implements a wrapper around trusted objects that allows them to // be safely injected into untrusted code. +namespace { + +const PRUint32 sPropIsReadable = 0x1; +const PRUint32 sPropIsWritable = 0x2; + +const PRUint32 sExposedPropsSlot = XPCWrapper::sNumSlots; + +class AutoIdArray { +public: + AutoIdArray(JSContext *cx, JSIdArray *ida) : cx(cx), ida(ida) { + } + + ~AutoIdArray() { + if (ida) { + JS_DestroyIdArray(cx, ida); + } + } + + JSIdArray *array() { + return ida; + } + +private: + JSContext *cx; + JSIdArray *ida; +}; + +JSBool +GetExposedProperties(JSContext *cx, JSObject *obj, jsval *rval) +{ + jsid exposedPropsId = GetRTIdByIndex(cx, XPCJSRuntime::IDX_EXPOSEDPROPS); + + JSBool found = JS_FALSE; + if (!JS_HasPropertyById(cx, obj, exposedPropsId, &found)) + return JS_FALSE; + if (!found) { + *rval = JSVAL_VOID; + return JS_TRUE; + } + + *rval = JSVAL_NULL; + jsval exposedProps; + if (!JS_LookupPropertyById(cx, obj, exposedPropsId, &exposedProps)) + return JS_FALSE; + + if (JSVAL_IS_VOID(exposedProps) || JSVAL_IS_NULL(exposedProps)) + return JS_TRUE; + + if (!JSVAL_IS_OBJECT(exposedProps)) { + JS_ReportError(cx, + "__exposedProps__ must be undefined, null, or an Object"); + return JS_FALSE; + } + + obj = JSVAL_TO_OBJECT(exposedProps); + + AutoIdArray guard(cx, JS_Enumerate(cx, obj)); + JSIdArray *props = guard.array(); + if (!props) + return JS_FALSE; + + if (props->length == 0) + return JS_TRUE; + + JSObject *info = JS_NewObjectWithGivenProto(cx, NULL, NULL, obj); + if (!info) + return JS_FALSE; + *rval = OBJECT_TO_JSVAL(info); + + for (int i = 0; i < props->length; i++) { + jsid propId = props->vector[i]; + + jsval propVal; + if (!JS_LookupPropertyById(cx, obj, propId, &propVal)) + return JS_FALSE; + + if (!JSVAL_IS_STRING(propVal)) { + JS_ReportError(cx, "property must be a string"); + return JS_FALSE; + } + + JSString *str = JSVAL_TO_STRING(propVal); + const jschar *chars = JS_GetStringChars(str); + size_t length = JS_GetStringLength(str); + int32 propPerms = 0; + for (size_t i = 0; i < length; ++i) { + switch (chars[i]) { + case 'r': + if (propPerms & sPropIsReadable) { + JS_ReportError(cx, "duplicate 'readable' property flag"); + return JS_FALSE; + } + propPerms |= sPropIsReadable; + break; + + case 'w': + if (propPerms & sPropIsWritable) { + JS_ReportError(cx, "duplicate 'writable' property flag"); + return JS_FALSE; + } + propPerms |= sPropIsWritable; + break; + + default: + JS_ReportError(cx, "properties can only be readable or read and writable"); + return JS_FALSE; + } + } + + if (propPerms == 0) { + JS_ReportError(cx, "specified properties must have a permission bit set"); + return JS_FALSE; + } + + if (!JS_DefinePropertyById(cx, info, propId, INT_TO_JSVAL(propPerms), + NULL, NULL, + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) { + return JS_FALSE; + } + } + + return JS_TRUE; +} + +JSBool +CanTouchProperty(JSContext *cx, JSObject *wrapperObj, jsid id, JSBool isSet, + JSBool *allowedp) +{ + jsval exposedProps; + if (!JS_GetReservedSlot(cx, wrapperObj, sExposedPropsSlot, &exposedProps)) { + return JS_FALSE; + } + + if (JSVAL_IS_PRIMITIVE(exposedProps)) { + // TODO For now, if the object doesn't ask for security, provide full + // access. In the future, we want to default to false here. + // NB: We differentiate between void (no __exposedProps__ property at all) + // and null (__exposedProps__ exists but didn't specify any properties) + // here. + *allowedp = JSVAL_IS_VOID(exposedProps); + return JS_TRUE; + } + + JSObject *hash = JSVAL_TO_OBJECT(exposedProps); + + jsval allowedval; + if (!JS_LookupPropertyById(cx, hash, id, &allowedval)) { + return JS_FALSE; + } + + const PRUint32 wanted = isSet ? sPropIsWritable : sPropIsReadable; + + // We test JSVAL_IS_INT to protect against unknown ids. + *allowedp = JSVAL_IS_INT(allowedval) && + (JSVAL_TO_INT(allowedval) & wanted) != 0; + + return JS_TRUE; +} + +} + static JSBool XPC_COW_AddProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp); @@ -87,7 +248,7 @@ JSExtendedClass sXPC_COW_JSClass = { // JSClass (JSExtendedClass.base) initialization { "ChromeObjectWrapper", JSCLASS_NEW_RESOLVE | JSCLASS_IS_EXTENDED | - JSCLASS_HAS_RESERVED_SLOTS(XPCWrapper::sNumSlots), + JSCLASS_HAS_RESERVED_SLOTS(XPCWrapper::sNumSlots + 1), XPC_COW_AddProperty, XPC_COW_DelProperty, XPC_COW_GetProperty, XPC_COW_SetProperty, XPC_COW_Enumerate, (JSResolveOp)XPC_COW_NewResolve, @@ -189,10 +350,9 @@ XPC_COW_FunctionWrapper(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, wrappedObj = obj; } - JSObject *funObj = JSVAL_TO_OBJECT(argv[-2]); jsval funToCall; - if (!JS_GetReservedSlot(cx, funObj, XPCWrapper::eWrappedFunctionSlot, - &funToCall)) { + if (!JS_GetReservedSlot(cx, JSVAL_TO_OBJECT(argv[-2]), + XPCWrapper::eWrappedFunctionSlot, &funToCall)) { return JS_FALSE; } @@ -370,18 +530,28 @@ XPC_COW_GetOrSetProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp, return ThrowException(NS_ERROR_ILLEGAL_VALUE, cx); } - if (id == GetRTStringByIndex(cx, XPCJSRuntime::IDX_PROTO) || - id == GetRTStringByIndex(cx, XPCJSRuntime::IDX_PARENT)) { + jsid interned_id; + if (!JS_ValueToId(cx, id, &interned_id)) { + return JS_FALSE; + } + + if (interned_id == GetRTIdByIndex(cx, XPCJSRuntime::IDX_PROTO) || + interned_id == GetRTIdByIndex(cx, XPCJSRuntime::IDX_PARENT) || + interned_id == GetRTIdByIndex(cx, XPCJSRuntime::IDX_EXPOSEDPROPS)) { // No getting or setting __proto__ or __parent__ on my object. return ThrowException(NS_ERROR_INVALID_ARG, cx); // XXX better error message } - if (!XPC_COW_RewrapForChrome(cx, obj, vp)) { + JSBool canTouch; + if (!CanTouchProperty(cx, obj, interned_id, isSet, &canTouch)) { return JS_FALSE; } - jsid interned_id; - if (!JS_ValueToId(cx, id, &interned_id)) { + if (!canTouch) { + return ThrowException(NS_ERROR_XPC_SECURITY_MANAGER_VETO, cx); + } + + if (!XPC_COW_RewrapForChrome(cx, obj, vp)) { return JS_FALSE; } @@ -426,7 +596,7 @@ XPC_COW_Enumerate(JSContext *cx, JSObject *obj) } static JSBool -XPC_COW_NewResolve(JSContext *cx, JSObject *obj, jsval id, uintN flags, +XPC_COW_NewResolve(JSContext *cx, JSObject *obj, jsval idval, uintN flags, JSObject **objp) { obj = GetWrapper(obj); @@ -443,6 +613,18 @@ XPC_COW_NewResolve(JSContext *cx, JSObject *obj, jsval id, uintN flags, return ThrowException(NS_ERROR_FAILURE, cx); } + jsid id; + JSBool canTouch; + if (!JS_ValueToId(cx, idval, &id) && + !CanTouchProperty(cx, obj, id, (flags & JSRESOLVE_ASSIGNING) != 0, + &canTouch)) { + return JS_FALSE; + } + + if (!canTouch) { + return ThrowException(NS_ERROR_XPC_SECURITY_MANAGER_VETO, cx); + } + return XPCWrapper::NewResolve(cx, obj, JS_TRUE, wrappedObj, id, flags, objp); } @@ -568,9 +750,18 @@ XPC_COW_WrapObject(JSContext *cx, JSObject *parent, jsval v, jsval *vp) } *vp = OBJECT_TO_JSVAL(wrapperObj); + + jsval exposedProps = JSVAL_VOID; + JSAutoTempValueRooter tvr(cx, 1, &exposedProps); + + if (!GetExposedProperties(cx, JSVAL_TO_OBJECT(v), &exposedProps)) { + return JS_FALSE; + } + if (!JS_SetReservedSlot(cx, wrapperObj, XPCWrapper::sWrappedObjSlot, v) || !JS_SetReservedSlot(cx, wrapperObj, XPCWrapper::sFlagsSlot, - JSVAL_ZERO)) { + JSVAL_ZERO) || + !JS_SetReservedSlot(cx, wrapperObj, sExposedPropsSlot, exposedProps)) { return JS_FALSE; } diff --git a/js/src/xpconnect/src/xpcinlines.h b/js/src/xpconnect/src/xpcinlines.h index 07afd2eae2de..ddfc76641ef5 100644 --- a/js/src/xpconnect/src/xpcinlines.h +++ b/js/src/xpconnect/src/xpcinlines.h @@ -757,11 +757,17 @@ xpc_SameScope(XPCWrappedNativeScope *objectscope, XPCWrappedNativeScope *xpcscop return JS_FALSE; } +inline jsid +GetRTIdByIndex(JSContext *cx, uintN index) +{ + XPCJSRuntime *rt = nsXPConnect::GetRuntimeInstance(); + return rt->GetStringID(index); +} + inline jsval GetRTStringByIndex(JSContext *cx, uintN index) { - XPCJSRuntime *rt = nsXPConnect::GetRuntimeInstance(); - return ID_TO_VALUE(rt->GetStringID(index)); + return ID_TO_VALUE(GetRTIdByIndex(cx, index)); } inline diff --git a/js/src/xpconnect/src/xpcjsruntime.cpp b/js/src/xpconnect/src/xpcjsruntime.cpp index 8f49d8ab71bc..005e1f02614e 100644 --- a/js/src/xpconnect/src/xpcjsruntime.cpp +++ b/js/src/xpconnect/src/xpcjsruntime.cpp @@ -62,7 +62,8 @@ const char* XPCJSRuntime::mStrings[] = { "item", // IDX_ITEM "__proto__", // IDX_PROTO "__iterator__", // IDX_ITERATOR - "__parent__" // IDX_PARENT + "__parent__", // IDX_PARENT + "__exposedProps__" // IDX_EXPOSEDPROPS }; /***************************************************************************/ diff --git a/js/src/xpconnect/src/xpcprivate.h b/js/src/xpconnect/src/xpcprivate.h index 6636cee429f8..2cccb7324d11 100644 --- a/js/src/xpconnect/src/xpcprivate.h +++ b/js/src/xpconnect/src/xpcprivate.h @@ -704,6 +704,7 @@ public: IDX_PROTO , IDX_ITERATOR , IDX_PARENT , + IDX_EXPOSEDPROPS , IDX_TOTAL_COUNT // just a count of the above }; @@ -4298,6 +4299,9 @@ xpc_EvalInSandbox(JSContext *cx, JSObject *sandbox, const nsAString& source, inline JSBool xpc_ForcePropertyResolve(JSContext* cx, JSObject* obj, jsval idval); +inline jsid +GetRTIdByIndex(JSContext *cx, uintN index); + inline jsval GetRTStringByIndex(JSContext *cx, uintN index);