michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* vim: set ts=8 sts=4 et sw=4 tw=99: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "AccessCheck.h" michael@0: michael@0: #include "nsJSPrincipals.h" michael@0: #include "nsGlobalWindow.h" michael@0: michael@0: #include "XPCWrapper.h" michael@0: #include "XrayWrapper.h" michael@0: michael@0: #include "jsfriendapi.h" michael@0: #include "mozilla/dom/BindingUtils.h" michael@0: #include "mozilla/dom/WindowBinding.h" michael@0: #include "nsIDOMWindowCollection.h" michael@0: #include "nsJSUtils.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace JS; michael@0: using namespace js; michael@0: michael@0: namespace xpc { michael@0: michael@0: nsIPrincipal * michael@0: GetCompartmentPrincipal(JSCompartment *compartment) michael@0: { michael@0: return nsJSPrincipals::get(JS_GetCompartmentPrincipals(compartment)); michael@0: } michael@0: michael@0: nsIPrincipal * michael@0: GetObjectPrincipal(JSObject *obj) michael@0: { michael@0: return GetCompartmentPrincipal(js::GetObjectCompartment(obj)); michael@0: } michael@0: michael@0: // Does the principal of compartment a subsume the principal of compartment b? michael@0: bool michael@0: AccessCheck::subsumes(JSCompartment *a, JSCompartment *b) michael@0: { michael@0: nsIPrincipal *aprin = GetCompartmentPrincipal(a); michael@0: nsIPrincipal *bprin = GetCompartmentPrincipal(b); michael@0: return aprin->Subsumes(bprin); michael@0: } michael@0: michael@0: bool michael@0: AccessCheck::subsumes(JSObject *a, JSObject *b) michael@0: { michael@0: return subsumes(js::GetObjectCompartment(a), js::GetObjectCompartment(b)); michael@0: } michael@0: michael@0: // Same as above, but considering document.domain. michael@0: bool michael@0: AccessCheck::subsumesConsideringDomain(JSCompartment *a, JSCompartment *b) michael@0: { michael@0: nsIPrincipal *aprin = GetCompartmentPrincipal(a); michael@0: nsIPrincipal *bprin = GetCompartmentPrincipal(b); michael@0: return aprin->SubsumesConsideringDomain(bprin); michael@0: } michael@0: michael@0: // Does the compartment of the wrapper subsumes the compartment of the wrappee? michael@0: bool michael@0: AccessCheck::wrapperSubsumes(JSObject *wrapper) michael@0: { michael@0: MOZ_ASSERT(js::IsWrapper(wrapper)); michael@0: JSObject *wrapped = js::UncheckedUnwrap(wrapper); michael@0: return AccessCheck::subsumes(js::GetObjectCompartment(wrapper), michael@0: js::GetObjectCompartment(wrapped)); michael@0: } michael@0: michael@0: bool michael@0: AccessCheck::isChrome(JSCompartment *compartment) michael@0: { michael@0: nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager(); michael@0: if (!ssm) { michael@0: return false; michael@0: } michael@0: michael@0: bool privileged; michael@0: nsIPrincipal *principal = GetCompartmentPrincipal(compartment); michael@0: return NS_SUCCEEDED(ssm->IsSystemPrincipal(principal, &privileged)) && privileged; michael@0: } michael@0: michael@0: bool michael@0: AccessCheck::isChrome(JSObject *obj) michael@0: { michael@0: return isChrome(js::GetObjectCompartment(obj)); michael@0: } michael@0: michael@0: bool michael@0: AccessCheck::callerIsChrome() michael@0: { michael@0: nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager(); michael@0: if (!ssm) michael@0: return false; michael@0: bool subjectIsSystem; michael@0: nsresult rv = ssm->SubjectPrincipalIsSystem(&subjectIsSystem); michael@0: return NS_SUCCEEDED(rv) && subjectIsSystem; michael@0: } michael@0: michael@0: nsIPrincipal * michael@0: AccessCheck::getPrincipal(JSCompartment *compartment) michael@0: { michael@0: return GetCompartmentPrincipal(compartment); michael@0: } michael@0: michael@0: #define NAME(ch, str, cases) \ michael@0: case ch: if (!strcmp(name, str)) switch (propChars[0]) { cases }; break; michael@0: #define PROP(ch, actions) case ch: { actions }; break; michael@0: #define RW(str) if (JS_FlatStringEqualsAscii(prop, str)) return true; michael@0: #define R(str) if (!set && JS_FlatStringEqualsAscii(prop, str)) return true; michael@0: #define W(str) if (set && JS_FlatStringEqualsAscii(prop, str)) return true; michael@0: michael@0: // Hardcoded policy for cross origin property access. This was culled from the michael@0: // preferences file (all.js). We don't want users to overwrite highly sensitive michael@0: // security policies. michael@0: static bool michael@0: IsPermitted(const char *name, JSFlatString *prop, bool set) michael@0: { michael@0: size_t propLength; michael@0: const jschar *propChars = michael@0: JS_GetInternedStringCharsAndLength(JS_FORGET_STRING_FLATNESS(prop), &propLength); michael@0: if (!propLength) michael@0: return false; michael@0: switch (name[0]) { michael@0: NAME('L', "Location", michael@0: PROP('h', W("href")) michael@0: PROP('r', R("replace"))) michael@0: case 'W': michael@0: if (!strcmp(name, "Window")) michael@0: return dom::WindowBinding::IsPermitted(prop, propChars[0], set); michael@0: break; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: #undef NAME michael@0: #undef RW michael@0: #undef R michael@0: #undef W michael@0: michael@0: static bool michael@0: IsFrameId(JSContext *cx, JSObject *objArg, jsid idArg) michael@0: { michael@0: RootedObject obj(cx, objArg); michael@0: RootedId id(cx, idArg); michael@0: michael@0: obj = JS_ObjectToInnerObject(cx, obj); michael@0: MOZ_ASSERT(!js::IsWrapper(obj)); michael@0: nsGlobalWindow* win = WindowOrNull(obj); michael@0: if (!win) { michael@0: return false; michael@0: } michael@0: michael@0: nsCOMPtr col; michael@0: win->GetFrames(getter_AddRefs(col)); michael@0: if (!col) { michael@0: return false; michael@0: } michael@0: michael@0: nsCOMPtr domwin; michael@0: if (JSID_IS_INT(id)) { michael@0: col->Item(JSID_TO_INT(id), getter_AddRefs(domwin)); michael@0: } else if (JSID_IS_STRING(id)) { michael@0: col->NamedItem(nsDependentJSString(id), getter_AddRefs(domwin)); michael@0: } michael@0: michael@0: return domwin != nullptr; michael@0: } michael@0: michael@0: static bool michael@0: IsWindow(const char *name) michael@0: { michael@0: return name[0] == 'W' && !strcmp(name, "Window"); michael@0: } michael@0: michael@0: bool michael@0: AccessCheck::isCrossOriginAccessPermitted(JSContext *cx, JSObject *wrapperArg, jsid idArg, michael@0: Wrapper::Action act) michael@0: { michael@0: if (!XPCWrapper::GetSecurityManager()) michael@0: return true; michael@0: michael@0: if (act == Wrapper::CALL) michael@0: return false; michael@0: michael@0: RootedId id(cx, idArg); michael@0: RootedObject wrapper(cx, wrapperArg); michael@0: RootedObject obj(cx, Wrapper::wrappedObject(wrapper)); michael@0: michael@0: // For XOWs, we generally want to deny enumerate-like operations, but fail michael@0: // silently (see CrossOriginAccessiblePropertiesOnly::deny). michael@0: if (act == Wrapper::ENUMERATE) michael@0: return false; michael@0: michael@0: const char *name; michael@0: const js::Class *clasp = js::GetObjectClass(obj); michael@0: MOZ_ASSERT(!XrayUtils::IsXPCWNHolderClass(Jsvalify(clasp)), "shouldn't have a holder here"); michael@0: if (clasp->ext.innerObject) michael@0: name = "Window"; michael@0: else michael@0: name = clasp->name; michael@0: michael@0: if (JSID_IS_STRING(id)) { michael@0: if (IsPermitted(name, JSID_TO_FLAT_STRING(id), act == Wrapper::SET)) michael@0: return true; michael@0: } michael@0: michael@0: if (act != Wrapper::GET) michael@0: return false; michael@0: michael@0: // Check for frame IDs. If we're resolving named frames, make sure to only michael@0: // resolve ones that don't shadow native properties. See bug 860494. michael@0: if (IsWindow(name)) { michael@0: if (JSID_IS_STRING(id) && !XrayUtils::IsXrayResolving(cx, wrapper, id)) { michael@0: bool wouldShadow = false; michael@0: if (!XrayUtils::HasNativeProperty(cx, wrapper, id, &wouldShadow) || michael@0: wouldShadow) michael@0: { michael@0: return false; michael@0: } michael@0: } michael@0: return IsFrameId(cx, obj, id); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: enum Access { READ = (1<<0), WRITE = (1<<1), NO_ACCESS = 0 }; michael@0: michael@0: static void michael@0: EnterAndThrow(JSContext *cx, JSObject *wrapper, const char *msg) michael@0: { michael@0: JSAutoCompartment ac(cx, wrapper); michael@0: JS_ReportError(cx, msg); michael@0: } michael@0: michael@0: bool michael@0: ExposedPropertiesOnly::check(JSContext *cx, JSObject *wrapperArg, jsid idArg, Wrapper::Action act) michael@0: { michael@0: RootedObject wrapper(cx, wrapperArg); michael@0: RootedId id(cx, idArg); michael@0: RootedObject wrappedObject(cx, Wrapper::wrappedObject(wrapper)); michael@0: michael@0: if (act == Wrapper::CALL) michael@0: return true; michael@0: michael@0: RootedId exposedPropsId(cx, GetRTIdByIndex(cx, XPCJSRuntime::IDX_EXPOSEDPROPS)); michael@0: michael@0: // We need to enter the wrappee's compartment to look at __exposedProps__, michael@0: // but we want to be in the wrapper's compartment if we call Deny(). michael@0: // michael@0: // Unfortunately, |cx| can be in either compartment when we call ::check. :-( michael@0: JSAutoCompartment ac(cx, wrappedObject); michael@0: michael@0: bool found = false; michael@0: if (!JS_HasPropertyById(cx, wrappedObject, exposedPropsId, &found)) michael@0: return false; michael@0: michael@0: // Always permit access to "length" and indexed properties of arrays. michael@0: if ((JS_IsArrayObject(cx, wrappedObject) || michael@0: JS_IsTypedArrayObject(wrappedObject)) && michael@0: ((JSID_IS_INT(id) && JSID_TO_INT(id) >= 0) || michael@0: (JSID_IS_STRING(id) && JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(id), "length")))) { michael@0: return true; // Allow michael@0: } michael@0: michael@0: // If no __exposedProps__ existed, deny access. michael@0: if (!found) { michael@0: return false; michael@0: } michael@0: michael@0: if (id == JSID_VOID) michael@0: return true; michael@0: michael@0: RootedValue exposedProps(cx); michael@0: if (!JS_LookupPropertyById(cx, wrappedObject, exposedPropsId, &exposedProps)) michael@0: return false; michael@0: michael@0: if (exposedProps.isNullOrUndefined()) michael@0: return false; michael@0: michael@0: if (!exposedProps.isObject()) { michael@0: EnterAndThrow(cx, wrapper, "__exposedProps__ must be undefined, null, or an Object"); michael@0: return false; michael@0: } michael@0: michael@0: RootedObject hallpass(cx, &exposedProps.toObject()); michael@0: michael@0: if (!AccessCheck::subsumes(js::UncheckedUnwrap(hallpass), wrappedObject)) { michael@0: EnterAndThrow(cx, wrapper, "Invalid __exposedProps__"); michael@0: return false; michael@0: } michael@0: michael@0: Access access = NO_ACCESS; michael@0: michael@0: Rooted desc(cx); michael@0: if (!JS_GetPropertyDescriptorById(cx, hallpass, id, &desc)) { michael@0: return false; // Error michael@0: } michael@0: if (!desc.object() || !desc.isEnumerable()) michael@0: return false; michael@0: michael@0: if (!desc.value().isString()) { michael@0: EnterAndThrow(cx, wrapper, "property must be a string"); michael@0: return false; michael@0: } michael@0: michael@0: JSString *str = desc.value().toString(); michael@0: size_t length; michael@0: const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length); michael@0: if (!chars) michael@0: return false; michael@0: michael@0: for (size_t i = 0; i < length; ++i) { michael@0: switch (chars[i]) { michael@0: case 'r': michael@0: if (access & READ) { michael@0: EnterAndThrow(cx, wrapper, "duplicate 'readable' property flag"); michael@0: return false; michael@0: } michael@0: access = Access(access | READ); michael@0: break; michael@0: michael@0: case 'w': michael@0: if (access & WRITE) { michael@0: EnterAndThrow(cx, wrapper, "duplicate 'writable' property flag"); michael@0: return false; michael@0: } michael@0: access = Access(access | WRITE); michael@0: break; michael@0: michael@0: default: michael@0: EnterAndThrow(cx, wrapper, "properties can only be readable or read and writable"); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if (access == NO_ACCESS) { michael@0: EnterAndThrow(cx, wrapper, "specified properties must have a permission bit set"); michael@0: return false; michael@0: } michael@0: michael@0: if ((act == Wrapper::SET && !(access & WRITE)) || michael@0: (act != Wrapper::SET && !(access & READ))) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bool michael@0: ExposedPropertiesOnly::allowNativeCall(JSContext *cx, JS::IsAcceptableThis test, michael@0: JS::NativeImpl impl) michael@0: { michael@0: return js::IsTypedArrayThisCheck(test); michael@0: } michael@0: michael@0: }