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