tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

MaybeCrossOriginObject.cpp (18521B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "mozilla/dom/MaybeCrossOriginObject.h"
      8 
      9 #include "AccessCheck.h"
     10 #include "js/CallAndConstruct.h"    // JS::Call
     11 #include "js/Object.h"              // JS::GetClass
     12 #include "js/PropertyAndElement.h"  // JS_DefineFunctions, JS_DefineProperties
     13 #include "js/PropertyDescriptor.h"  // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById
     14 #include "js/Proxy.h"
     15 #include "js/RootingAPI.h"
     16 #include "js/WeakMap.h"
     17 #include "js/Wrapper.h"
     18 #include "js/friend/WindowProxy.h"  // js::IsWindowProxy
     19 #include "jsfriendapi.h"
     20 #include "mozilla/BasePrincipal.h"
     21 #include "mozilla/dom/BindingUtils.h"
     22 #include "mozilla/dom/DOMJSProxyHandler.h"
     23 #include "mozilla/dom/RemoteObjectProxy.h"
     24 #include "nsContentUtils.h"
     25 
     26 #ifdef DEBUG
     27 static bool IsLocation(JSObject* obj) {
     28  return strcmp(JS::GetClass(obj)->name, "Location") == 0;
     29 }
     30 #endif  // DEBUG
     31 
     32 namespace mozilla::dom {
     33 
     34 /* static */
     35 bool MaybeCrossOriginObjectMixins::IsPlatformObjectSameOrigin(JSContext* cx,
     36                                                              JSObject* obj) {
     37  MOZ_ASSERT(!js::IsCrossCompartmentWrapper(obj));
     38  // WindowProxy and Window must always be same-Realm, so we can do
     39  // our IsPlatformObjectSameOrigin check against either one.  But verify that
     40  // in case we have a WindowProxy the right things happen.
     41  MOZ_ASSERT(js::GetNonCCWObjectRealm(obj) ==
     42                 // "true" for second arg means to unwrap WindowProxy to
     43                 // get at the Window.
     44                 js::GetNonCCWObjectRealm(js::UncheckedUnwrap(obj, true)),
     45             "WindowProxy not same-Realm as Window?");
     46 
     47  BasePrincipal* subjectPrincipal =
     48      BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(cx));
     49  BasePrincipal* objectPrincipal =
     50      BasePrincipal::Cast(nsContentUtils::ObjectPrincipal(obj));
     51 
     52  // The spec effectively has an EqualsConsideringDomain check here,
     53  // because the spec has no concept of asymmetric security
     54  // relationships.  But we shouldn't ever end up here in the
     55  // asymmetric case anyway: That case should end up with Xrays, which
     56  // don't call into this code.
     57  //
     58  // Let's assert that EqualsConsideringDomain and
     59  // SubsumesConsideringDomain give the same results and use
     60  // EqualsConsideringDomain for the check we actually do, since it's
     61  // stricter and more closely matches the spec.
     62  //
     63  // That said, if the (not very well named)
     64  // OriginAttributes::IsRestrictOpenerAccessForFPI() method returns
     65  // false, we want to use FastSubsumesConsideringDomainIgnoringFPD
     66  // instead of FastEqualsConsideringDomain, because in that case we
     67  // still want to treat things which are in different first-party
     68  // contexts as same-origin.
     69  MOZ_ASSERT(
     70      subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal) ==
     71          subjectPrincipal->FastSubsumesConsideringDomain(objectPrincipal),
     72      "Why are we in an asymmetric case here?");
     73  if (OriginAttributes::IsRestrictOpenerAccessForFPI()) {
     74    return subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal);
     75  }
     76 
     77  return subjectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
     78             objectPrincipal) &&
     79         objectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
     80             subjectPrincipal);
     81 }
     82 
     83 bool MaybeCrossOriginObjectMixins::CrossOriginGetOwnPropertyHelper(
     84    JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
     85    JS::MutableHandle<Maybe<JS::PropertyDescriptor>> desc) const {
     86  MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
     87             "Why did we get called?");
     88  // First check for an IDL-defined cross-origin property with the given name.
     89  // This corresponds to
     90  // https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)
     91  // step 2.
     92  JS::Rooted<JSObject*> holder(cx);
     93  if (!EnsureHolder(cx, obj, &holder)) {
     94    return false;
     95  }
     96 
     97  return JS_GetOwnPropertyDescriptorById(cx, holder, id, desc);
     98 }
     99 
    100 /* static */
    101 bool MaybeCrossOriginObjectMixins::CrossOriginPropertyFallback(
    102    JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
    103    JS::MutableHandle<Maybe<JS::PropertyDescriptor>> desc) {
    104  MOZ_ASSERT(desc.isNothing(), "Why are we being called?");
    105 
    106  // Step 1.
    107  if (xpc::IsCrossOriginWhitelistedProp(cx, id)) {
    108    // Spec says to return PropertyDescriptor {
    109    //   [[Value]]: undefined, [[Writable]]: false, [[Enumerable]]: false,
    110    //   [[Configurable]]: true
    111    // }.
    112    desc.set(Some(JS::PropertyDescriptor::Data(
    113        JS::UndefinedValue(), {JS::PropertyAttribute::Configurable})));
    114    return true;
    115  }
    116 
    117  // Step 2.
    118  return ReportCrossOriginDenial(cx, id, "access"_ns);
    119 }
    120 
    121 /* static */
    122 bool MaybeCrossOriginObjectMixins::CrossOriginGet(
    123    JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<JS::Value> receiver,
    124    JS::Handle<jsid> id, JS::MutableHandle<JS::Value> vp) {
    125  // This is fairly similar to BaseProxyHandler::get, but there are some
    126  // differences.  Most importantly, we want to throw if we have a descriptor
    127  // with no getter, while BaseProxyHandler::get returns undefined.  The other
    128  // big difference is that we don't have to worry about prototypes (ours is
    129  // always null).
    130 
    131  // We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
    132  // compartment, because for the proxies we have here [[GetOwnProperty]] will
    133  // do security checks based on the current Realm.  Unfortunately,
    134  // JS_GetPropertyDescriptorById asserts that compartments match.  Luckily, we
    135  // know that "obj" is a proxy here, so we can directly call its
    136  // getOwnPropertyDescriptor() hook.
    137  //
    138  // It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
    139  // the handler and call its getOwnPropertyDescriptor hook directly.
    140  MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
    141  MOZ_ASSERT(
    142      js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
    143      "Unexpected proxy");
    144  MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
    145             "Why did we get called?");
    146  js::AssertSameCompartment(cx, receiver);
    147 
    148  // Step 1.
    149  JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
    150  if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
    151    return false;
    152  }
    153 
    154  // Step 2.
    155  MOZ_ASSERT(desc.isSome(),
    156             "Callees should throw in all cases when they are not finding a "
    157             "property decriptor");
    158  desc->assertComplete();
    159 
    160  // Step 3.
    161  if (desc->isDataDescriptor()) {
    162    vp.set(desc->value());
    163    return true;
    164  }
    165 
    166  // Step 4.
    167  MOZ_ASSERT(desc->isAccessorDescriptor());
    168 
    169  // Step 5.
    170  JS::Rooted<JSObject*> getter(cx);
    171  if (!desc->hasGetter() || !(getter = desc->getter())) {
    172    // Step 6.
    173    return ReportCrossOriginDenial(cx, id, "get"_ns);
    174  }
    175 
    176  // Step 7.
    177  return JS::Call(cx, receiver, getter, JS::HandleValueArray::empty(), vp);
    178 }
    179 
    180 /* static */
    181 bool MaybeCrossOriginObjectMixins::CrossOriginSet(
    182    JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
    183    JS::Handle<JS::Value> v, JS::Handle<JS::Value> receiver,
    184    JS::ObjectOpResult& result) {
    185  // We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
    186  // compartment, because for the proxies we have here [[GetOwnProperty]] will
    187  // do security checks based on the current Realm.  Unfortunately,
    188  // JS_GetPropertyDescriptorById asserts that compartments match.  Luckily, we
    189  // know that "obj" is a proxy here, so we can directly call its
    190  // getOwnPropertyDescriptor() hook.
    191  //
    192  // It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
    193  // the handler and call its getOwnPropertyDescriptor hook directly.
    194  MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
    195  MOZ_ASSERT(
    196      js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
    197      "Unexpected proxy");
    198  MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
    199             "Why did we get called?");
    200  js::AssertSameCompartment(cx, receiver);
    201  js::AssertSameCompartment(cx, v);
    202 
    203  // Step 1.
    204  JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
    205  if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
    206    return false;
    207  }
    208 
    209  // Step 2.
    210  MOZ_ASSERT(desc.isSome(),
    211             "Callees should throw in all cases when they are not finding a "
    212             "property decriptor");
    213  desc->assertComplete();
    214 
    215  // Step 3.
    216  JS::Rooted<JSObject*> setter(cx);
    217  if (desc->hasSetter() && (setter = desc->setter())) {
    218    JS::Rooted<JS::Value> ignored(cx);
    219    // Step 3.1.
    220    if (!JS::Call(cx, receiver, setter, JS::HandleValueArray(v), &ignored)) {
    221      return false;
    222    }
    223 
    224    // Step 3.2.
    225    return result.succeed();
    226  }
    227 
    228  // Step 4.
    229  return ReportCrossOriginDenial(cx, id, "set"_ns);
    230 }
    231 
    232 /* static */
    233 bool MaybeCrossOriginObjectMixins::EnsureHolder(
    234    JSContext* cx, JS::Handle<JSObject*> obj, size_t slot,
    235    const CrossOriginProperties& properties,
    236    JS::MutableHandle<JSObject*> holder) {
    237  MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
    238             "Why are we calling this at all in same-origin cases?");
    239  // We store the holders in a weakmap stored in obj's slot.  Our object is
    240  // always a proxy, so we can just go ahead and use GetProxyReservedSlot here.
    241  JS::Rooted<JS::Value> weakMapVal(cx, js::GetProxyReservedSlot(obj, slot));
    242  if (weakMapVal.isUndefined()) {
    243    // Enter the Realm of "obj" when we allocate the WeakMap, since we are going
    244    // to store it in a slot on "obj" and in general we may not be
    245    // same-compartment with "obj" here.
    246    JSAutoRealm ar(cx, obj);
    247    JSObject* newMap = JS::NewWeakMapObject(cx);
    248    if (!newMap) {
    249      return false;
    250    }
    251    weakMapVal.setObject(*newMap);
    252    js::SetProxyReservedSlot(obj, slot, weakMapVal);
    253  }
    254  MOZ_ASSERT(weakMapVal.isObject(),
    255             "How did a non-object else end up in this slot?");
    256 
    257  JS::Rooted<JSObject*> map(cx, &weakMapVal.toObject());
    258  MOZ_ASSERT(JS::IsWeakMapObject(map),
    259             "How did something else end up in this slot?");
    260 
    261  // We need to be in "map"'s compartment to work with it.  Per spec, the key
    262  // for this map is supposed to be the pair (current settings, relevant
    263  // settings).  The current settings corresponds to the current Realm of cx.
    264  // The relevant settings corresponds to the Realm of "obj", but since all of
    265  // our objects are per-Realm singletons, we are basically using "obj" itself
    266  // as part of the key.
    267  //
    268  // To represent the current settings, we use a dedicated key object of the
    269  // current-Realm.
    270  //
    271  // We can't use the current global, because we can't get a useful
    272  // cross-compartment wrapper for it; such wrappers would always go
    273  // through a WindowProxy and would not be guarantee to keep pointing to a
    274  // single Realm when unwrapped.  We want to grab this key before we start
    275  // changing Realms.
    276  //
    277  // Also we can't use arbitrary object (e.g.: Object.prototype), because at
    278  // this point those compartments are not same-origin, and don't have access to
    279  // each other, and the object retrieved here will be wrapped by a security
    280  // wrapper below, and the wrapper will be stored into the cache
    281  // (see Compartment::wrap).  Those compartments can get access later by
    282  // modifying `document.domain`, and wrapping objects after that point
    283  // shouldn't result in a security wrapper.  Wrap operation looks up the
    284  // existing wrapper in the cache, that contains the security wrapper created
    285  // here.  We should use unique/private object here, so that this doesn't
    286  // affect later wrap operation.
    287  JS::Rooted<JSObject*> key(cx, JS::GetRealmKeyObject(cx));
    288  if (!key) {
    289    return false;
    290  }
    291 
    292  JS::Rooted<JS::Value> holderVal(cx);
    293  {  // Scope for working with the map
    294    JSAutoRealm ar(cx, map);
    295    if (!MaybeWrapObject(cx, &key)) {
    296      return false;
    297    }
    298 
    299    JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
    300    if (!JS::GetWeakMapEntry(cx, map, keyVal, &holderVal)) {
    301      return false;
    302    }
    303  }
    304 
    305  if (holderVal.isObject()) {
    306    // We want to do an unchecked unwrap, because the holder (and the current
    307    // caller) may actually be more privileged than our map.
    308    holder.set(js::UncheckedUnwrap(&holderVal.toObject()));
    309 
    310    // holder might be a dead object proxy if things got nuked.
    311    if (!JS_IsDeadWrapper(holder)) {
    312      MOZ_ASSERT(js::GetContextRealm(cx) == js::GetNonCCWObjectRealm(holder),
    313                 "How did we end up with a key/value mismatch?");
    314      return true;
    315    }
    316  }
    317 
    318  // We didn't find a usable holder.  Go ahead and allocate one.  At this point
    319  // we have two options: we could allocate the holder in the current Realm and
    320  // store a cross-compartment wrapper for it in the map as needed, or we could
    321  // allocate the holder in the Realm of the map and have it hold
    322  // cross-compartment references to all the methods it holds, since those
    323  // methods need to be in our current Realm.  It seems better to allocate the
    324  // holder in our current Realm.
    325  bool isChrome = xpc::AccessCheck::isChrome(js::GetContextRealm(cx));
    326  holder.set(JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
    327  if (!holder || !JS_DefineProperties(cx, holder, properties.mAttributes) ||
    328      !JS_DefineFunctions(cx, holder, properties.mMethods) ||
    329      (isChrome && properties.mChromeOnlyAttributes &&
    330       !JS_DefineProperties(cx, holder, properties.mChromeOnlyAttributes)) ||
    331      (isChrome && properties.mChromeOnlyMethods &&
    332       !JS_DefineFunctions(cx, holder, properties.mChromeOnlyMethods))) {
    333    return false;
    334  }
    335 
    336  holderVal.setObject(*holder);
    337  {  // Scope for working with the map
    338    JSAutoRealm ar(cx, map);
    339 
    340    // Key is already in the right Realm, but we need to wrap the value.
    341    if (!MaybeWrapValue(cx, &holderVal)) {
    342      return false;
    343    }
    344 
    345    JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
    346    if (!JS::SetWeakMapEntry(cx, map, keyVal, holderVal)) {
    347      return false;
    348    }
    349  }
    350 
    351  return true;
    352 }
    353 
    354 /* static */
    355 bool MaybeCrossOriginObjectMixins::ReportCrossOriginDenial(
    356    JSContext* aCx, JS::Handle<jsid> aId, const nsACString& aAccessType) {
    357  xpc::AccessCheck::reportCrossOriginDenial(aCx, aId, aAccessType);
    358  return false;
    359 }
    360 
    361 template <typename Base>
    362 bool MaybeCrossOriginObject<Base>::getPrototype(
    363    JSContext* cx, JS::Handle<JSObject*> proxy,
    364    JS::MutableHandle<JSObject*> protop) const {
    365  if (!IsPlatformObjectSameOrigin(cx, proxy)) {
    366    protop.set(nullptr);
    367    return true;
    368  }
    369 
    370  {  // Scope for JSAutoRealm
    371    JSAutoRealm ar(cx, proxy);
    372    protop.set(getSameOriginPrototype(cx));
    373    if (!protop) {
    374      return false;
    375    }
    376  }
    377 
    378  return MaybeWrapObject(cx, protop);
    379 }
    380 
    381 template <typename Base>
    382 bool MaybeCrossOriginObject<Base>::setPrototype(
    383    JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<JSObject*> proto,
    384    JS::ObjectOpResult& result) const {
    385  // Inlined version of
    386  // https://tc39.github.io/ecma262/#sec-set-immutable-prototype
    387  js::AssertSameCompartment(cx, proto);
    388 
    389  // We have to be careful how we get the prototype.  In particular, we do _NOT_
    390  // want to enter the Realm of "proxy" to do that, in case we're not
    391  // same-origin with it here.
    392  JS::Rooted<JSObject*> wrappedProxy(cx, proxy);
    393  if (!MaybeWrapObject(cx, &wrappedProxy)) {
    394    return false;
    395  }
    396 
    397  JS::Rooted<JSObject*> currentProto(cx);
    398  if (!js::GetObjectProto(cx, wrappedProxy, &currentProto)) {
    399    return false;
    400  }
    401 
    402  if (currentProto != proto) {
    403    return result.failCantSetProto();
    404  }
    405 
    406  return result.succeed();
    407 }
    408 
    409 template <typename Base>
    410 bool MaybeCrossOriginObject<Base>::getPrototypeIfOrdinary(
    411    JSContext* cx, JS::Handle<JSObject*> proxy, bool* isOrdinary,
    412    JS::MutableHandle<JSObject*> protop) const {
    413  // We have a custom [[GetPrototypeOf]]
    414  *isOrdinary = false;
    415  return true;
    416 }
    417 
    418 template <typename Base>
    419 bool MaybeCrossOriginObject<Base>::setImmutablePrototype(
    420    JSContext* cx, JS::Handle<JSObject*> proxy, bool* succeeded) const {
    421  // We just want to disallow this.
    422  *succeeded = false;
    423  return true;
    424 }
    425 
    426 template <typename Base>
    427 bool MaybeCrossOriginObject<Base>::isExtensible(JSContext* cx,
    428                                                JS::Handle<JSObject*> proxy,
    429                                                bool* extensible) const {
    430  // We never allow [[PreventExtensions]] to succeed.
    431  *extensible = true;
    432  return true;
    433 }
    434 
    435 template <typename Base>
    436 bool MaybeCrossOriginObject<Base>::preventExtensions(
    437    JSContext* cx, JS::Handle<JSObject*> proxy,
    438    JS::ObjectOpResult& result) const {
    439  return result.failCantPreventExtensions();
    440 }
    441 
    442 template <typename Base>
    443 bool MaybeCrossOriginObject<Base>::defineProperty(
    444    JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
    445    JS::Handle<JS::PropertyDescriptor> desc, JS::ObjectOpResult& result) const {
    446  if (!IsPlatformObjectSameOrigin(cx, proxy)) {
    447    return ReportCrossOriginDenial(cx, id, "define"_ns);
    448  }
    449 
    450  // Enter the Realm of proxy and do the remaining work in there.
    451  JSAutoRealm ar(cx, proxy);
    452  JS::Rooted<JS::PropertyDescriptor> descCopy(cx, desc);
    453  if (!JS_WrapPropertyDescriptor(cx, &descCopy)) {
    454    return false;
    455  }
    456 
    457  JS_MarkCrossZoneId(cx, id);
    458 
    459  return definePropertySameOrigin(cx, proxy, id, descCopy, result);
    460 }
    461 
    462 template <typename Base>
    463 bool MaybeCrossOriginObject<Base>::enumerate(
    464    JSContext* cx, JS::Handle<JSObject*> proxy,
    465    JS::MutableHandleVector<jsid> props) const {
    466  // Just get the property keys from ourselves, in whatever Realm we happen to
    467  // be in. It's important to not enter the Realm of "proxy" here, because that
    468  // would affect the list of keys we claim to have. We wrap the proxy in the
    469  // current compartment just to be safe; it doesn't affect behavior as far as
    470  // CrossOriginObjectWrapper and MaybeCrossOriginObject are concerned.
    471  JS::Rooted<JSObject*> self(cx, proxy);
    472  if (!MaybeWrapObject(cx, &self)) {
    473    return false;
    474  }
    475 
    476  return js::GetPropertyKeys(cx, self, 0, props);
    477 }
    478 
    479 // Force instantiations of the out-of-line template methods we need.
    480 template class MaybeCrossOriginObject<js::Wrapper>;
    481 template class MaybeCrossOriginObject<DOMProxyHandler>;
    482 
    483 }  // namespace mozilla::dom