js/xpconnect/wrappers/AccessCheck.cpp
author Andrew McCreight <continuation@gmail.com>
Thu, 07 Sep 2017 11:17:16 -0700
changeset 380157 cdb4e0259eeb
parent 380156 0a058c19c235
permissions -rw-r--r--
Bug 1377587, part 2 - Rename ExposedPropertiesOnly to OpaqueWithSilentFailing. r=krizsa This class doesn't have anything to do with __exposedProps__ any more, so give it a more descriptive name. We'd still like to remove it entirely eventually. MozReview-Commit-ID: 87KCpG6f8rI
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim: set ts=8 sts=4 et sw=4 tw=99: */
/* 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 "BasePrincipal.h"
#include "nsGlobalWindow.h"

#include "XPCWrapper.h"
#include "XrayWrapper.h"
#include "FilteringWrapper.h"

#include "jsfriendapi.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/LocationBinding.h"
#include "mozilla/dom/WindowBinding.h"
#include "mozilla/jsipc/CrossProcessObjectWrappers.h"
#include "nsIDOMWindowCollection.h"
#include "nsJSUtils.h"
#include "xpcprivate.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 BasePrincipal::Cast(aprin)->FastSubsumes(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)
{
    MOZ_ASSERT(OriginAttributes::IsRestrictOpenerAccessForFPI());
    nsIPrincipal* aprin = GetCompartmentPrincipal(a);
    nsIPrincipal* bprin = GetCompartmentPrincipal(b);
    return BasePrincipal::Cast(aprin)->FastSubsumesConsideringDomain(bprin);
}

bool
AccessCheck::subsumesConsideringDomainIgnoringFPD(JSCompartment* a,
                                                  JSCompartment* b)
{
    MOZ_ASSERT(!OriginAttributes::IsRestrictOpenerAccessForFPI());
    nsIPrincipal* aprin = GetCompartmentPrincipal(a);
    nsIPrincipal* bprin = GetCompartmentPrincipal(b);
    return BasePrincipal::Cast(aprin)->FastSubsumesConsideringDomainIgnoringFPD(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)
{
    nsIPrincipal* principal = GetCompartmentPrincipal(compartment);
    return nsXPConnect::SystemPrincipal() == principal;
}

bool
AccessCheck::isChrome(JSObject* obj)
{
    return isChrome(js::GetObjectCompartment(obj));
}

nsIPrincipal*
AccessCheck::getPrincipal(JSCompartment* compartment)
{
    return GetCompartmentPrincipal(compartment);
}

// Hardcoded policy for cross origin property access. See the HTML5 Spec.
static bool
IsPermitted(CrossOriginObjectType type, JSFlatString* prop, bool set)
{
    size_t propLength = JS_GetStringLength(JS_FORGET_STRING_FLATNESS(prop));
    if (!propLength)
        return false;

    char16_t propChar0 = JS_GetFlatStringCharAt(prop, 0);
    if (type == CrossOriginLocation)
        return dom::LocationBinding::IsPermitted(prop, propChar0, set);
    if (type == CrossOriginWindow)
        return dom::WindowBinding::IsPermitted(prop, propChar0, set);

    return false;
}

static bool
IsFrameId(JSContext* cx, JSObject* obj, jsid idArg)
{
    MOZ_ASSERT(!js::IsWrapper(obj));
    RootedId id(cx, idArg);

    nsGlobalWindow* win = WindowOrNull(obj);
    if (!win) {
        return false;
    }

    nsCOMPtr<nsIDOMWindowCollection> col = win->GetFrames();
    if (!col) {
        return false;
    }

    nsCOMPtr<mozIDOMWindowProxy> domwin;
    if (JSID_IS_INT(id)) {
        col->Item(JSID_TO_INT(id), getter_AddRefs(domwin));
    } else if (JSID_IS_STRING(id)) {
        nsAutoJSString idAsString;
        if (!idAsString.init(cx, JSID_TO_STRING(id))) {
            return false;
        }
        col->NamedItem(idAsString, getter_AddRefs(domwin));
    }

    return domwin != nullptr;
}

CrossOriginObjectType
IdentifyCrossOriginObject(JSObject* obj)
{
    obj = js::UncheckedUnwrap(obj, /* stopAtWindowProxy = */ false);
    const js::Class* clasp = js::GetObjectClass(obj);
    MOZ_ASSERT(!XrayUtils::IsXPCWNHolderClass(Jsvalify(clasp)), "shouldn't have a holder here");

    if (clasp->name[0] == 'L' && !strcmp(clasp->name, "Location"))
        return CrossOriginLocation;
    if (clasp->name[0] == 'W' && !strcmp(clasp->name, "Window"))
        return CrossOriginWindow;

    return CrossOriginOpaque;
}

bool
AccessCheck::isCrossOriginAccessPermitted(JSContext* cx, HandleObject wrapper, HandleId id,
                                          Wrapper::Action act)
{
    if (act == Wrapper::CALL)
        return false;

    if (act == Wrapper::ENUMERATE)
        return true;

    // For the case of getting a property descriptor, we allow if either GET or SET
    // is allowed, and rely on FilteringWrapper to filter out any disallowed accessors.
    if (act == Wrapper::GET_PROPERTY_DESCRIPTOR) {
        return isCrossOriginAccessPermitted(cx, wrapper, id, Wrapper::GET) ||
               isCrossOriginAccessPermitted(cx, wrapper, id, Wrapper::SET);
    }

    RootedObject obj(cx, js::UncheckedUnwrap(wrapper, /* stopAtWindowProxy = */ false));
    CrossOriginObjectType type = IdentifyCrossOriginObject(obj);
    if (JSID_IS_STRING(id)) {
        if (IsPermitted(type, JSID_TO_FLAT_STRING(id), act == Wrapper::SET))
            return true;
    } else if (type != CrossOriginOpaque &&
               IsCrossOriginWhitelistedSymbol(cx, id)) {
        // We always allow access to @@toStringTag, @@hasInstance, and
        // @@isConcatSpreadable.  But then we nerf them to be a value descriptor
        // with value undefined in CrossOriginXrayWrapper.
        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 (type == CrossOriginWindow) {
        if (JSID_IS_STRING(id)) {
            bool wouldShadow = false;
            if (!XrayUtils::HasNativeProperty(cx, wrapper, id, &wouldShadow) ||
                wouldShadow)
            {
                // If the named subframe matches the name of a DOM constructor,
                // the global resolve triggered by the HasNativeProperty call
                // above will try to perform a CheckedUnwrap on |wrapper|, and
                // throw a security error if it fails. That exception isn't
                // really useful for our callers, so we silence it and just
                // deny access to the property (since it matched a builtin).
                //
                // Note that this would be a problem if the resolve code ever
                // tried to CheckedUnwrap the wrapper _before_ concluding that
                // the name corresponds to a builtin global property, since it
                // would mean that we'd never permit cross-origin named subframe
                // access (something we regrettably need to support).
                JS_ClearPendingException(cx);
                return false;
            }
        }
        return IsFrameId(cx, obj, id);
    }
    return false;
}

bool
AccessCheck::checkPassToPrivilegedCode(JSContext* cx, HandleObject wrapper, HandleValue v)
{
    // Primitives are fine.
    if (!v.isObject())
        return true;
    RootedObject obj(cx, &v.toObject());

    // Non-wrappers are fine.
    if (!js::IsWrapper(obj))
        return true;

    // CPOWs use COWs (in the unprivileged junk scope) for all child->parent
    // references. Without this test, the child process wouldn't be able to
    // pass any objects at all to CPOWs.
    if (mozilla::jsipc::IsWrappedCPOW(obj) &&
        js::GetObjectCompartment(wrapper) == js::GetObjectCompartment(xpc::UnprivilegedJunkScope()) &&
        XRE_IsParentProcess())
    {
        return true;
    }

    // Same-origin wrappers are fine.
    if (AccessCheck::wrapperSubsumes(obj))
        return true;

    // Badness.
    JS_ReportErrorASCII(cx, "Permission denied to pass object to privileged code");
    return false;
}

bool
AccessCheck::checkPassToPrivilegedCode(JSContext* cx, HandleObject wrapper, const CallArgs& args)
{
    if (!checkPassToPrivilegedCode(cx, wrapper, args.thisv()))
        return false;
    for (size_t i = 0; i < args.length(); ++i) {
        if (!checkPassToPrivilegedCode(cx, wrapper, args[i]))
            return false;
    }
    return true;
}

void
AccessCheck::reportCrossOriginDenial(JSContext* cx, JS::HandleId id,
                                     const nsACString& accessType)
{
    // This function exists because we want to report DOM SecurityErrors, not JS
    // Errors, when denying access on cross-origin DOM objects.  It's
    // conceptually pretty similar to
    // AutoEnterPolicy::reportErrorIfExceptionIsNotPending.
    if (JS_IsExceptionPending(cx)) {
        return;
    }

    nsAutoCString message;
    if (JSID_IS_VOID(id)) {
        message = NS_LITERAL_CSTRING("Permission denied to access object");
    } else {
        // We want to use JS_ValueToSource here, because that most closely
        // matches what AutoEnterPolicy::reportErrorIfExceptionIsNotPending
        // does.
        JS::RootedValue idVal(cx, js::IdToValue(id));
        nsAutoJSString propName;
        JS::RootedString idStr(cx, JS_ValueToSource(cx, idVal));
        if (!idStr || !propName.init(cx, idStr)) {
            return;
        }
        message = NS_LITERAL_CSTRING("Permission denied to ") +
                  accessType +
                  NS_LITERAL_CSTRING(" property ") +
                  NS_ConvertUTF16toUTF8(propName) +
                  NS_LITERAL_CSTRING(" on cross-origin object");
    }
    ErrorResult rv;
    rv.ThrowDOMException(NS_ERROR_DOM_SECURITY_ERR, message);
    MOZ_ALWAYS_TRUE(rv.MaybeSetPendingException(cx));
}

bool
OpaqueWithSilentFailing::deny(JSContext* cx, js::Wrapper::Action act, HandleId id,
                              bool mayThrow)
{
    // Fail silently for GET, ENUMERATE, and GET_PROPERTY_DESCRIPTOR.
    if (act == js::Wrapper::GET || act == js::Wrapper::ENUMERATE ||
        act == js::Wrapper::GET_PROPERTY_DESCRIPTOR)
    {
        // Note that ReportWrapperDenial doesn't do any _exception_ reporting,
        // so we want to do this regardless of the value of mayThrow.
        return ReportWrapperDenial(cx, id, WrapperDenialForCOW,
                                   "Access to privileged JS object not permitted");
    }

    return false;
}

} // namespace xpc