Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / dom / base / MaybeCrossOriginObject.cpp
blob5c9eb227c60d0f7a820e14e477a2515a8899f214
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/. */
7 #include "mozilla/dom/MaybeCrossOriginObject.h"
9 #include "mozilla/BasePrincipal.h"
10 #include "mozilla/dom/BindingUtils.h"
11 #include "mozilla/dom/DOMJSProxyHandler.h"
12 #include "mozilla/dom/RemoteObjectProxy.h"
13 #include "js/CallAndConstruct.h" // JS::Call
14 #include "js/friend/WindowProxy.h" // js::IsWindowProxy
15 #include "js/Object.h" // JS::GetClass
16 #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
17 #include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById
18 #include "js/Proxy.h"
19 #include "js/RootingAPI.h"
20 #include "js/WeakMap.h"
21 #include "js/Wrapper.h"
22 #include "jsfriendapi.h"
23 #include "AccessCheck.h"
24 #include "nsContentUtils.h"
26 #ifdef DEBUG
27 static bool IsLocation(JSObject* obj) {
28 return strcmp(JS::GetClass(obj)->name, "Location") == 0;
30 #endif // DEBUG
32 namespace mozilla::dom {
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?");
47 BasePrincipal* subjectPrincipal =
48 BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(cx));
49 BasePrincipal* objectPrincipal =
50 BasePrincipal::Cast(nsContentUtils::ObjectPrincipal(obj));
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.
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.
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);
77 return subjectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
78 objectPrincipal) &&
79 objectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
80 subjectPrincipal);
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;
97 return JS_GetOwnPropertyDescriptorById(cx, holder, id, desc);
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?");
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;
117 // Step 2.
118 return ReportCrossOriginDenial(cx, id, "access"_ns);
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).
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.
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);
148 // Step 1.
149 JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
150 if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
151 return false;
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();
160 // Step 3.
161 if (desc->isDataDescriptor()) {
162 vp.set(desc->value());
163 return true;
166 // Step 4.
167 MOZ_ASSERT(desc->isAccessorDescriptor());
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);
176 // Step 7.
177 return JS::Call(cx, receiver, getter, JS::HandleValueArray::empty(), vp);
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.
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);
203 // Step 1.
204 JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(cx);
205 if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
206 return false;
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();
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;
224 // Step 3.2.
225 return result.succeed();
228 // Step 4.
229 return ReportCrossOriginDenial(cx, id, "set"_ns);
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;
251 weakMapVal.setObject(*newMap);
252 js::SetProxyReservedSlot(obj, slot, weakMapVal);
254 MOZ_ASSERT(weakMapVal.isObject(),
255 "How did a non-object else end up in this slot?");
257 JS::Rooted<JSObject*> map(cx, &weakMapVal.toObject());
258 MOZ_ASSERT(JS::IsWeakMapObject(map),
259 "How did something else end up in this slot?");
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.
268 // To represent the current settings, we use a dedicated key object of the
269 // current-Realm.
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.
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;
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;
299 JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
300 if (!JS::GetWeakMapEntry(cx, map, keyVal, &holderVal)) {
301 return false;
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()));
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;
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;
336 holderVal.setObject(*holder);
337 { // Scope for working with the map
338 JSAutoRealm ar(cx, map);
340 // Key is already in the right Realm, but we need to wrap the value.
341 if (!MaybeWrapValue(cx, &holderVal)) {
342 return false;
345 JS::Rooted<JS::Value> keyVal(cx, JS::ObjectValue(*key));
346 if (!JS::SetWeakMapEntry(cx, map, keyVal, holderVal)) {
347 return false;
351 return true;
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;
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;
370 { // Scope for JSAutoRealm
371 JSAutoRealm ar(cx, proxy);
372 protop.set(getSameOriginPrototype(cx));
373 if (!protop) {
374 return false;
378 return MaybeWrapObject(cx, protop);
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);
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;
397 JS::Rooted<JSObject*> currentProto(cx);
398 if (!js::GetObjectProto(cx, wrappedProxy, &currentProto)) {
399 return false;
402 if (currentProto != proto) {
403 return result.failCantSetProto();
406 return result.succeed();
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;
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;
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;
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();
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);
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;
457 JS_MarkCrossZoneId(cx, id);
459 return definePropertySameOrigin(cx, proxy, id, descCopy, result);
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;
476 return js::GetPropertyKeys(cx, self, 0, props);
479 // Force instantiations of the out-of-line template methods we need.
480 template class MaybeCrossOriginObject<js::Wrapper>;
481 template class MaybeCrossOriginObject<DOMProxyHandler>;
483 } // namespace mozilla::dom