mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
5b78cbc27c
Differential Revision: https://phabricator.services.mozilla.com/D192583
484 lines
18 KiB
C++
484 lines
18 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 "mozilla/dom/MaybeCrossOriginObject.h"
|
|
|
|
#include "mozilla/BasePrincipal.h"
|
|
#include "mozilla/dom/BindingUtils.h"
|
|
#include "mozilla/dom/DOMJSProxyHandler.h"
|
|
#include "mozilla/dom/RemoteObjectProxy.h"
|
|
#include "js/CallAndConstruct.h" // JS::Call
|
|
#include "js/friend/WindowProxy.h" // js::IsWindowProxy
|
|
#include "js/Object.h" // JS::GetClass
|
|
#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
|
|
#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById
|
|
#include "js/Proxy.h"
|
|
#include "js/RootingAPI.h"
|
|
#include "js/WeakMap.h"
|
|
#include "js/Wrapper.h"
|
|
#include "jsfriendapi.h"
|
|
#include "AccessCheck.h"
|
|
#include "nsContentUtils.h"
|
|
|
|
#ifdef DEBUG
|
|
static bool IsLocation(JSObject* obj) {
|
|
return strcmp(JS::GetClass(obj)->name, "Location") == 0;
|
|
}
|
|
#endif // DEBUG
|
|
|
|
namespace mozilla::dom {
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::IsPlatformObjectSameOrigin(JSContext* cx,
|
|
JSObject* obj) {
|
|
MOZ_ASSERT(!js::IsCrossCompartmentWrapper(obj));
|
|
// WindowProxy and Window must always be same-Realm, so we can do
|
|
// our IsPlatformObjectSameOrigin check against either one. But verify that
|
|
// in case we have a WindowProxy the right things happen.
|
|
MOZ_ASSERT(js::GetNonCCWObjectRealm(obj) ==
|
|
// "true" for second arg means to unwrap WindowProxy to
|
|
// get at the Window.
|
|
js::GetNonCCWObjectRealm(js::UncheckedUnwrap(obj, true)),
|
|
"WindowProxy not same-Realm as Window?");
|
|
|
|
BasePrincipal* subjectPrincipal =
|
|
BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(cx));
|
|
BasePrincipal* objectPrincipal =
|
|
BasePrincipal::Cast(nsContentUtils::ObjectPrincipal(obj));
|
|
|
|
// The spec effectively has an EqualsConsideringDomain check here,
|
|
// because the spec has no concept of asymmetric security
|
|
// relationships. But we shouldn't ever end up here in the
|
|
// asymmetric case anyway: That case should end up with Xrays, which
|
|
// don't call into this code.
|
|
//
|
|
// Let's assert that EqualsConsideringDomain and
|
|
// SubsumesConsideringDomain give the same results and use
|
|
// EqualsConsideringDomain for the check we actually do, since it's
|
|
// stricter and more closely matches the spec.
|
|
//
|
|
// That said, if the (not very well named)
|
|
// OriginAttributes::IsRestrictOpenerAccessForFPI() method returns
|
|
// false, we want to use FastSubsumesConsideringDomainIgnoringFPD
|
|
// instead of FastEqualsConsideringDomain, because in that case we
|
|
// still want to treat things which are in different first-party
|
|
// contexts as same-origin.
|
|
MOZ_ASSERT(
|
|
subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal) ==
|
|
subjectPrincipal->FastSubsumesConsideringDomain(objectPrincipal),
|
|
"Why are we in an asymmetric case here?");
|
|
if (OriginAttributes::IsRestrictOpenerAccessForFPI()) {
|
|
return subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal);
|
|
}
|
|
|
|
return subjectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
|
|
objectPrincipal) &&
|
|
objectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
|
|
subjectPrincipal);
|
|
}
|
|
|
|
bool MaybeCrossOriginObjectMixins::CrossOriginGetOwnPropertyHelper(
|
|
JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
|
|
JS::MutableHandle<Maybe<JS::PropertyDescriptor>> desc) const {
|
|
MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
|
|
"Why did we get called?");
|
|
// First check for an IDL-defined cross-origin property with the given name.
|
|
// This corresponds to
|
|
// https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)
|
|
// step 2.
|
|
JS::Rooted<JSObject*> holder(cx);
|
|
if (!EnsureHolder(cx, obj, &holder)) {
|
|
return false;
|
|
}
|
|
|
|
return JS_GetOwnPropertyDescriptorById(cx, holder, id, desc);
|
|
}
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::CrossOriginPropertyFallback(
|
|
JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
|
|
JS::MutableHandle<Maybe<JS::PropertyDescriptor>> desc) {
|
|
MOZ_ASSERT(desc.isNothing(), "Why are we being called?");
|
|
|
|
// Step 1.
|
|
if (xpc::IsCrossOriginWhitelistedProp(cx, id)) {
|
|
// Spec says to return PropertyDescriptor {
|
|
// [[Value]]: undefined, [[Writable]]: false, [[Enumerable]]: false,
|
|
// [[Configurable]]: true
|
|
// }.
|
|
desc.set(Some(JS::PropertyDescriptor::Data(
|
|
JS::UndefinedValue(), {JS::PropertyAttribute::Configurable})));
|
|
return true;
|
|
}
|
|
|
|
// Step 2.
|
|
return ReportCrossOriginDenial(cx, id, "access"_ns);
|
|
}
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::CrossOriginGet(
|
|
JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<JS::Value> receiver,
|
|
JS::Handle<jsid> id, JS::MutableHandle<JS::Value> vp) {
|
|
// This is fairly similar to BaseProxyHandler::get, but there are some
|
|
// differences. Most importantly, we want to throw if we have a descriptor
|
|
// with no getter, while BaseProxyHandler::get returns undefined. The other
|
|
// big difference is that we don't have to worry about prototypes (ours is
|
|
// always null).
|
|
|
|
// We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
|
|
// compartment, because for the proxies we have here [[GetOwnProperty]] will
|
|
// do security checks based on the current Realm. Unfortunately,
|
|
// JS_GetPropertyDescriptorById asserts that compartments match. Luckily, we
|
|
// know that "obj" is a proxy here, so we can directly call its
|
|
// getOwnPropertyDescriptor() hook.
|
|
//
|
|
// It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
|
|
// the handler and call its getOwnPropertyDescriptor hook directly.
|
|
MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
|
|
MOZ_ASSERT(
|
|
js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
|
|
"Unexpected proxy");
|
|
MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
|
|
"Why did we get called?");
|
|
js::AssertSameCompartment(cx, receiver);
|
|
|
|
// Step 1.
|
|
JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
|
|
if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
|
|
return false;
|
|
}
|
|
|
|
// Step 2.
|
|
MOZ_ASSERT(desc.isSome(),
|
|
"Callees should throw in all cases when they are not finding a "
|
|
"property decriptor");
|
|
desc->assertComplete();
|
|
|
|
// Step 3.
|
|
if (desc->isDataDescriptor()) {
|
|
vp.set(desc->value());
|
|
return true;
|
|
}
|
|
|
|
// Step 4.
|
|
MOZ_ASSERT(desc->isAccessorDescriptor());
|
|
|
|
// Step 5.
|
|
JS::Rooted<JSObject*> getter(cx);
|
|
if (!desc->hasGetter() || !(getter = desc->getter())) {
|
|
// Step 6.
|
|
return ReportCrossOriginDenial(cx, id, "get"_ns);
|
|
}
|
|
|
|
// Step 7.
|
|
return JS::Call(cx, receiver, getter, JS::HandleValueArray::empty(), vp);
|
|
}
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::CrossOriginSet(
|
|
JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
|
|
JS::Handle<JS::Value> v, JS::Handle<JS::Value> receiver,
|
|
JS::ObjectOpResult& result) {
|
|
// We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
|
|
// compartment, because for the proxies we have here [[GetOwnProperty]] will
|
|
// do security checks based on the current Realm. Unfortunately,
|
|
// JS_GetPropertyDescriptorById asserts that compartments match. Luckily, we
|
|
// know that "obj" is a proxy here, so we can directly call its
|
|
// getOwnPropertyDescriptor() hook.
|
|
//
|
|
// It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
|
|
// the handler and call its getOwnPropertyDescriptor hook directly.
|
|
MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
|
|
MOZ_ASSERT(
|
|
js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
|
|
"Unexpected proxy");
|
|
MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
|
|
"Why did we get called?");
|
|
js::AssertSameCompartment(cx, receiver);
|
|
js::AssertSameCompartment(cx, v);
|
|
|
|
// Step 1.
|
|
JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
|
|
if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
|
|
return false;
|
|
}
|
|
|
|
// Step 2.
|
|
MOZ_ASSERT(desc.isSome(),
|
|
"Callees should throw in all cases when they are not finding a "
|
|
"property decriptor");
|
|
desc->assertComplete();
|
|
|
|
// Step 3.
|
|
JS::Rooted<JSObject*> setter(cx);
|
|
if (desc->hasSetter() && (setter = desc->setter())) {
|
|
JS::Rooted<JS::Value> ignored(cx);
|
|
// Step 3.1.
|
|
if (!JS::Call(cx, receiver, setter, JS::HandleValueArray(v), &ignored)) {
|
|
return false;
|
|
}
|
|
|
|
// Step 3.2.
|
|
return result.succeed();
|
|
}
|
|
|
|
// Step 4.
|
|
return ReportCrossOriginDenial(cx, id, "set"_ns);
|
|
}
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::EnsureHolder(
|
|
JSContext* cx, JS::Handle<JSObject*> obj, size_t slot,
|
|
const CrossOriginProperties& properties,
|
|
JS::MutableHandle<JSObject*> holder) {
|
|
MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
|
|
"Why are we calling this at all in same-origin cases?");
|
|
// We store the holders in a weakmap stored in obj's slot. Our object is
|
|
// always a proxy, so we can just go ahead and use GetProxyReservedSlot here.
|
|
JS::Rooted<JS::Value> weakMapVal(cx, js::GetProxyReservedSlot(obj, slot));
|
|
if (weakMapVal.isUndefined()) {
|
|
// Enter the Realm of "obj" when we allocate the WeakMap, since we are going
|
|
// to store it in a slot on "obj" and in general we may not be
|
|
// same-compartment with "obj" here.
|
|
JSAutoRealm ar(cx, obj);
|
|
JSObject* newMap = JS::NewWeakMapObject(cx);
|
|
if (!newMap) {
|
|
return false;
|
|
}
|
|
weakMapVal.setObject(*newMap);
|
|
js::SetProxyReservedSlot(obj, slot, weakMapVal);
|
|
}
|
|
MOZ_ASSERT(weakMapVal.isObject(),
|
|
"How did a non-object else end up in this slot?");
|
|
|
|
JS::Rooted<JSObject*> map(cx, &weakMapVal.toObject());
|
|
MOZ_ASSERT(JS::IsWeakMapObject(map),
|
|
"How did something else end up in this slot?");
|
|
|
|
// We need to be in "map"'s compartment to work with it. Per spec, the key
|
|
// for this map is supposed to be the pair (current settings, relevant
|
|
// settings). The current settings corresponds to the current Realm of cx.
|
|
// The relevant settings corresponds to the Realm of "obj", but since all of
|
|
// our objects are per-Realm singletons, we are basically using "obj" itself
|
|
// as part of the key.
|
|
//
|
|
// To represent the current settings, we use a dedicated key object of the
|
|
// current-Realm.
|
|
//
|
|
// We can't use the current global, because we can't get a useful
|
|
// cross-compartment wrapper for it; such wrappers would always go
|
|
// through a WindowProxy and would not be guarantee to keep pointing to a
|
|
// single Realm when unwrapped. We want to grab this key before we start
|
|
// changing Realms.
|
|
//
|
|
// Also we can't use arbitrary object (e.g.: Object.prototype), because at
|
|
// this point those compartments are not same-origin, and don't have access to
|
|
// each other, and the object retrieved here will be wrapped by a security
|
|
// wrapper below, and the wrapper will be stored into the cache
|
|
// (see Compartment::wrap). Those compartments can get access later by
|
|
// modifying `document.domain`, and wrapping objects after that point
|
|
// shouldn't result in a security wrapper. Wrap operation looks up the
|
|
// existing wrapper in the cache, that contains the security wrapper created
|
|
// here. We should use unique/private object here, so that this doesn't
|
|
// affect later wrap operation.
|
|
JS::Rooted<JSObject*> key(cx, JS::GetRealmKeyObject(cx));
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> holderVal(cx);
|
|
{ // Scope for working with the map
|
|
JSAutoRealm ar(cx, map);
|
|
if (!MaybeWrapObject(cx, &key)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
|
|
if (!JS::GetWeakMapEntry(cx, map, keyVal, &holderVal)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (holderVal.isObject()) {
|
|
// We want to do an unchecked unwrap, because the holder (and the current
|
|
// caller) may actually be more privileged than our map.
|
|
holder.set(js::UncheckedUnwrap(&holderVal.toObject()));
|
|
|
|
// holder might be a dead object proxy if things got nuked.
|
|
if (!JS_IsDeadWrapper(holder)) {
|
|
MOZ_ASSERT(js::GetContextRealm(cx) == js::GetNonCCWObjectRealm(holder),
|
|
"How did we end up with a key/value mismatch?");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// We didn't find a usable holder. Go ahead and allocate one. At this point
|
|
// we have two options: we could allocate the holder in the current Realm and
|
|
// store a cross-compartment wrapper for it in the map as needed, or we could
|
|
// allocate the holder in the Realm of the map and have it hold
|
|
// cross-compartment references to all the methods it holds, since those
|
|
// methods need to be in our current Realm. It seems better to allocate the
|
|
// holder in our current Realm.
|
|
bool isChrome = xpc::AccessCheck::isChrome(js::GetContextRealm(cx));
|
|
holder.set(JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
|
|
if (!holder || !JS_DefineProperties(cx, holder, properties.mAttributes) ||
|
|
!JS_DefineFunctions(cx, holder, properties.mMethods) ||
|
|
(isChrome && properties.mChromeOnlyAttributes &&
|
|
!JS_DefineProperties(cx, holder, properties.mChromeOnlyAttributes)) ||
|
|
(isChrome && properties.mChromeOnlyMethods &&
|
|
!JS_DefineFunctions(cx, holder, properties.mChromeOnlyMethods))) {
|
|
return false;
|
|
}
|
|
|
|
holderVal.setObject(*holder);
|
|
{ // Scope for working with the map
|
|
JSAutoRealm ar(cx, map);
|
|
|
|
// Key is already in the right Realm, but we need to wrap the value.
|
|
if (!MaybeWrapValue(cx, &holderVal)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
|
|
if (!JS::SetWeakMapEntry(cx, map, keyVal, holderVal)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */
|
|
bool MaybeCrossOriginObjectMixins::ReportCrossOriginDenial(
|
|
JSContext* aCx, JS::Handle<jsid> aId, const nsACString& aAccessType) {
|
|
xpc::AccessCheck::reportCrossOriginDenial(aCx, aId, aAccessType);
|
|
return false;
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::getPrototype(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy,
|
|
JS::MutableHandle<JSObject*> protop) const {
|
|
if (!IsPlatformObjectSameOrigin(cx, proxy)) {
|
|
protop.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
{ // Scope for JSAutoRealm
|
|
JSAutoRealm ar(cx, proxy);
|
|
protop.set(getSameOriginPrototype(cx));
|
|
if (!protop) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return MaybeWrapObject(cx, protop);
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::setPrototype(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<JSObject*> proto,
|
|
JS::ObjectOpResult& result) const {
|
|
// Inlined version of
|
|
// https://tc39.github.io/ecma262/#sec-set-immutable-prototype
|
|
js::AssertSameCompartment(cx, proto);
|
|
|
|
// We have to be careful how we get the prototype. In particular, we do _NOT_
|
|
// want to enter the Realm of "proxy" to do that, in case we're not
|
|
// same-origin with it here.
|
|
JS::Rooted<JSObject*> wrappedProxy(cx, proxy);
|
|
if (!MaybeWrapObject(cx, &wrappedProxy)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> currentProto(cx);
|
|
if (!js::GetObjectProto(cx, wrappedProxy, ¤tProto)) {
|
|
return false;
|
|
}
|
|
|
|
if (currentProto != proto) {
|
|
return result.failCantSetProto();
|
|
}
|
|
|
|
return result.succeed();
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::getPrototypeIfOrdinary(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy, bool* isOrdinary,
|
|
JS::MutableHandle<JSObject*> protop) const {
|
|
// We have a custom [[GetPrototypeOf]]
|
|
*isOrdinary = false;
|
|
return true;
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::setImmutablePrototype(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy, bool* succeeded) const {
|
|
// We just want to disallow this.
|
|
*succeeded = false;
|
|
return true;
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::isExtensible(JSContext* cx,
|
|
JS::Handle<JSObject*> proxy,
|
|
bool* extensible) const {
|
|
// We never allow [[PreventExtensions]] to succeed.
|
|
*extensible = true;
|
|
return true;
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::preventExtensions(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy,
|
|
JS::ObjectOpResult& result) const {
|
|
return result.failCantPreventExtensions();
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::defineProperty(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
|
|
JS::Handle<JS::PropertyDescriptor> desc, JS::ObjectOpResult& result) const {
|
|
if (!IsPlatformObjectSameOrigin(cx, proxy)) {
|
|
return ReportCrossOriginDenial(cx, id, "define"_ns);
|
|
}
|
|
|
|
// Enter the Realm of proxy and do the remaining work in there.
|
|
JSAutoRealm ar(cx, proxy);
|
|
JS::Rooted<JS::PropertyDescriptor> descCopy(cx, desc);
|
|
if (!JS_WrapPropertyDescriptor(cx, &descCopy)) {
|
|
return false;
|
|
}
|
|
|
|
JS_MarkCrossZoneId(cx, id);
|
|
|
|
return definePropertySameOrigin(cx, proxy, id, descCopy, result);
|
|
}
|
|
|
|
template <typename Base>
|
|
bool MaybeCrossOriginObject<Base>::enumerate(
|
|
JSContext* cx, JS::Handle<JSObject*> proxy,
|
|
JS::MutableHandleVector<jsid> props) const {
|
|
// Just get the property keys from ourselves, in whatever Realm we happen to
|
|
// be in. It's important to not enter the Realm of "proxy" here, because that
|
|
// would affect the list of keys we claim to have. We wrap the proxy in the
|
|
// current compartment just to be safe; it doesn't affect behavior as far as
|
|
// CrossOriginObjectWrapper and MaybeCrossOriginObject are concerned.
|
|
JS::Rooted<JSObject*> self(cx, proxy);
|
|
if (!MaybeWrapObject(cx, &self)) {
|
|
return false;
|
|
}
|
|
|
|
return js::GetPropertyKeys(cx, self, 0, props);
|
|
}
|
|
|
|
// Force instantiations of the out-of-line template methods we need.
|
|
template class MaybeCrossOriginObject<js::Wrapper>;
|
|
template class MaybeCrossOriginObject<DOMProxyHandler>;
|
|
|
|
} // namespace mozilla::dom
|