summaryrefslogtreecommitdiffstats
path: root/dom/base/MaybeCrossOriginObject.cpp
blob: 5c9eb227c60d0f7a820e14e477a2515a8899f214 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
/* -*- 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, &currentProto)) {
    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