Bug 1665252 - remove allowpaymentrequest attribute from HTMLIFrameElement r=dom-worke...
[gecko.git] / dom / base / MaybeCrossOriginObject.cpp
blobf6e6a9643018ada89e8316d3ad227388163c0a18
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/friend/WindowProxy.h" // js::IsWindowProxy
14 #include "js/Object.h" // JS::GetClass
15 #include "js/Proxy.h"
16 #include "js/RootingAPI.h"
17 #include "js/Wrapper.h"
18 #include "jsfriendapi.h"
19 #include "AccessCheck.h"
20 #include "nsContentUtils.h"
22 #ifdef DEBUG
23 static bool IsLocation(JSObject* obj) {
24 return strcmp(JS::GetClass(obj)->name, "Location") == 0;
26 #endif // DEBUG
28 namespace mozilla {
29 namespace dom {
31 /* static */
32 bool MaybeCrossOriginObjectMixins::IsPlatformObjectSameOrigin(JSContext* cx,
33 JSObject* obj) {
34 MOZ_ASSERT(!js::IsCrossCompartmentWrapper(obj));
35 // WindowProxy and Window must always be same-Realm, so we can do
36 // our IsPlatformObjectSameOrigin check against either one. But verify that
37 // in case we have a WindowProxy the right things happen.
38 MOZ_ASSERT(js::GetNonCCWObjectRealm(obj) ==
39 // "true" for second arg means to unwrap WindowProxy to
40 // get at the Window.
41 js::GetNonCCWObjectRealm(js::UncheckedUnwrap(obj, true)),
42 "WindowProxy not same-Realm as Window?");
44 BasePrincipal* subjectPrincipal =
45 BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(cx));
46 BasePrincipal* objectPrincipal =
47 BasePrincipal::Cast(nsContentUtils::ObjectPrincipal(obj));
49 // The spec effectively has an EqualsConsideringDomain check here,
50 // because the spec has no concept of asymmetric security
51 // relationships. But we shouldn't ever end up here in the
52 // asymmetric case anyway: That case should end up with Xrays, which
53 // don't call into this code.
55 // Let's assert that EqualsConsideringDomain and
56 // SubsumesConsideringDomain give the same results and use
57 // EqualsConsideringDomain for the check we actually do, since it's
58 // stricter and more closely matches the spec.
60 // That said, if the (not very well named)
61 // OriginAttributes::IsRestrictOpenerAccessForFPI() method returns
62 // false, we want to use FastSubsumesConsideringDomainIgnoringFPD
63 // instead of FastEqualsConsideringDomain, because in that case we
64 // still want to treat things which are in different first-party
65 // contexts as same-origin.
66 MOZ_ASSERT(
67 subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal) ==
68 subjectPrincipal->FastSubsumesConsideringDomain(objectPrincipal),
69 "Why are we in an asymmetric case here?");
70 if (OriginAttributes::IsRestrictOpenerAccessForFPI()) {
71 return subjectPrincipal->FastEqualsConsideringDomain(objectPrincipal);
74 return subjectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
75 objectPrincipal) &&
76 objectPrincipal->FastSubsumesConsideringDomainIgnoringFPD(
77 subjectPrincipal);
80 bool MaybeCrossOriginObjectMixins::CrossOriginGetOwnPropertyHelper(
81 JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
82 JS::MutableHandle<JS::PropertyDescriptor> desc) const {
83 MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
84 "Why did we get called?");
85 // First check for an IDL-defined cross-origin property with the given name.
86 // This corresponds to
87 // https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)
88 // step 2.
89 JS::Rooted<JSObject*> holder(cx);
90 if (!EnsureHolder(cx, obj, &holder)) {
91 return false;
94 if (!JS_GetOwnPropertyDescriptorById(cx, holder, id, desc)) {
95 return false;
98 if (desc.object()) {
99 desc.object().set(obj);
102 return true;
105 /* static */
106 bool MaybeCrossOriginObjectMixins::CrossOriginPropertyFallback(
107 JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
108 JS::MutableHandle<JS::PropertyDescriptor> desc) {
109 MOZ_ASSERT(!desc.object(), "Why are we being called?");
111 // Step 1.
112 if (xpc::IsCrossOriginWhitelistedProp(cx, id)) {
113 // Spec says to return PropertyDescriptor {
114 // [[Value]]: undefined, [[Writable]]: false, [[Enumerable]]: false,
115 // [[Configurable]]: true
116 // }.
117 desc.setDataDescriptor(JS::UndefinedHandleValue, JSPROP_READONLY);
118 desc.object().set(obj);
119 return true;
122 // Step 2.
123 return ReportCrossOriginDenial(cx, id, "access"_ns);
126 /* static */
127 bool MaybeCrossOriginObjectMixins::CrossOriginGet(
128 JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<JS::Value> receiver,
129 JS::Handle<jsid> id, JS::MutableHandle<JS::Value> vp) {
130 // This is fairly similar to BaseProxyHandler::get, but there are some
131 // differences. Most importantly, we want to throw if we have a descriptor
132 // with no getter, while BaseProxyHandler::get returns undefined. The other
133 // big difference is that we don't have to worry about prototypes (ours is
134 // always null).
136 // We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
137 // compartment, because for the proxies we have here [[GetOwnProperty]] will
138 // do security checks based on the current Realm. Unfortunately,
139 // JS_GetPropertyDescriptorById asserts that compartments match. Luckily, we
140 // know that "obj" is a proxy here, so we can directly call its
141 // getOwnPropertyDescriptor() hook.
143 // It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
144 // the handler and call its getOwnPropertyDescriptor hook directly.
145 MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
146 MOZ_ASSERT(
147 js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
148 "Unexpected proxy");
149 MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
150 "Why did we get called?");
151 js::AssertSameCompartment(cx, receiver);
153 // Step 1.
154 JS::Rooted<JS::PropertyDescriptor> desc(cx);
155 if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
156 return false;
158 desc.assertCompleteIfFound();
160 // Step 2.
161 MOZ_ASSERT(desc.object(),
162 "Callees should throw in all cases when they are not finding a "
163 "property decriptor");
165 // Step 3.
166 if (desc.isDataDescriptor()) {
167 vp.set(desc.value());
168 return true;
171 // Step 4.
172 MOZ_ASSERT(desc.isAccessorDescriptor());
174 // Step 5.
175 JS::Rooted<JSObject*> getter(cx);
176 if (!desc.hasGetterObject() || !(getter = desc.getterObject())) {
177 // Step 6.
178 return ReportCrossOriginDenial(cx, id, "get"_ns);
181 // Step 7.
182 return JS::Call(cx, receiver, getter, JS::HandleValueArray::empty(), vp);
185 /* static */
186 bool MaybeCrossOriginObjectMixins::CrossOriginSet(
187 JSContext* cx, JS::Handle<JSObject*> obj, JS::Handle<jsid> id,
188 JS::Handle<JS::Value> v, JS::Handle<JS::Value> receiver,
189 JS::ObjectOpResult& result) {
190 // We want to invoke [[GetOwnProperty]] on "obj", but _without_ entering its
191 // compartment, because for the proxies we have here [[GetOwnProperty]] will
192 // do security checks based on the current Realm. Unfortunately,
193 // JS_GetPropertyDescriptorById asserts that compartments match. Luckily, we
194 // know that "obj" is a proxy here, so we can directly call its
195 // getOwnPropertyDescriptor() hook.
197 // It looks like Proxy::getOwnPropertyDescriptor is not public, so just grab
198 // the handler and call its getOwnPropertyDescriptor hook directly.
199 MOZ_ASSERT(js::IsProxy(obj), "How did we get a bogus object here?");
200 MOZ_ASSERT(
201 js::IsWindowProxy(obj) || IsLocation(obj) || IsRemoteObjectProxy(obj),
202 "Unexpected proxy");
203 MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
204 "Why did we get called?");
205 js::AssertSameCompartment(cx, receiver);
206 js::AssertSameCompartment(cx, v);
208 // Step 1.
209 JS::Rooted<JS::PropertyDescriptor> desc(cx);
210 if (!js::GetProxyHandler(obj)->getOwnPropertyDescriptor(cx, obj, id, &desc)) {
211 return false;
213 desc.assertCompleteIfFound();
215 // Step 2.
216 MOZ_ASSERT(desc.object(),
217 "Callees should throw in all cases when they are not finding a "
218 "property decriptor");
220 // Step 3.
221 JS::Rooted<JSObject*> setter(cx);
222 if (desc.hasSetterObject() && (setter = desc.setterObject())) {
223 JS::Rooted<JS::Value> ignored(cx);
224 // Step 3.1.
225 if (!JS::Call(cx, receiver, setter, JS::HandleValueArray(v), &ignored)) {
226 return false;
229 // Step 3.2.
230 return result.succeed();
233 // Step 4.
234 return ReportCrossOriginDenial(cx, id, "set"_ns);
237 /* static */
238 bool MaybeCrossOriginObjectMixins::EnsureHolder(
239 JSContext* cx, JS::Handle<JSObject*> obj, size_t slot,
240 const CrossOriginProperties& properties,
241 JS::MutableHandle<JSObject*> holder) {
242 MOZ_ASSERT(!IsPlatformObjectSameOrigin(cx, obj) || IsRemoteObjectProxy(obj),
243 "Why are we calling this at all in same-origin cases?");
244 // We store the holders in a weakmap stored in obj's slot. Our object is
245 // always a proxy, so we can just go ahead and use GetProxyReservedSlot here.
246 JS::Rooted<JS::Value> weakMapVal(cx, js::GetProxyReservedSlot(obj, slot));
247 if (weakMapVal.isUndefined()) {
248 // Enter the Realm of "obj" when we allocate the WeakMap, since we are going
249 // to store it in a slot on "obj" and in general we may not be
250 // same-compartment with "obj" here.
251 JSAutoRealm ar(cx, obj);
252 JSObject* newMap = JS::NewWeakMapObject(cx);
253 if (!newMap) {
254 return false;
256 weakMapVal.setObject(*newMap);
257 js::SetProxyReservedSlot(obj, slot, weakMapVal);
259 MOZ_ASSERT(weakMapVal.isObject(),
260 "How did a non-object else end up in this slot?");
262 JS::Rooted<JSObject*> map(cx, &weakMapVal.toObject());
263 MOZ_ASSERT(JS::IsWeakMapObject(map),
264 "How did something else end up in this slot?");
266 // We need to be in "map"'s compartment to work with it. Per spec, the key
267 // for this map is supposed to be the pair (current settings, relevant
268 // settings). The current settings corresponds to the current Realm of cx.
269 // The relevant settings corresponds to the Realm of "obj", but since all of
270 // our objects are per-Realm singletons, we are basically using "obj" itself
271 // as part of the key.
273 // To represent the current settings, we use the current-Realm
274 // Object.prototype. We can't use the current global, because we can't get a
275 // useful cross-compartment wrapper for it; such wrappers would always go
276 // through a WindowProxy and would not be guarantee to keep pointing to a
277 // single Realm when unwrapped. We want to grab this key before we start
278 // changing Realms.
279 JS::Rooted<JSObject*> key(cx, JS::GetRealmObjectPrototype(cx));
280 if (!key) {
281 return false;
284 JS::Rooted<JS::Value> holderVal(cx);
285 { // Scope for working with the map
286 JSAutoRealm ar(cx, map);
287 if (!MaybeWrapObject(cx, &key)) {
288 return false;
291 if (!JS::GetWeakMapEntry(cx, map, key, &holderVal)) {
292 return false;
296 if (holderVal.isObject()) {
297 // We want to do an unchecked unwrap, because the holder (and the current
298 // caller) may actually be more privileged than our map.
299 holder.set(js::UncheckedUnwrap(&holderVal.toObject()));
301 // holder might be a dead object proxy if things got nuked.
302 if (!JS_IsDeadWrapper(holder)) {
303 MOZ_ASSERT(js::GetContextRealm(cx) == js::GetNonCCWObjectRealm(holder),
304 "How did we end up with a key/value mismatch?");
305 return true;
309 // We didn't find a usable holder. Go ahead and allocate one. At this point
310 // we have two options: we could allocate the holder in the current Realm and
311 // store a cross-compartment wrapper for it in the map as needed, or we could
312 // allocate the holder in the Realm of the map and have it hold
313 // cross-compartment references to all the methods it holds, since those
314 // methods need to be in our current Realm. It seems better to allocate the
315 // holder in our current Realm.
316 bool isChrome = xpc::AccessCheck::isChrome(js::GetContextRealm(cx));
317 holder.set(JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
318 if (!holder || !JS_DefineProperties(cx, holder, properties.mAttributes) ||
319 !JS_DefineFunctions(cx, holder, properties.mMethods) ||
320 (isChrome && properties.mChromeOnlyAttributes &&
321 !JS_DefineProperties(cx, holder, properties.mChromeOnlyAttributes)) ||
322 (isChrome && properties.mChromeOnlyMethods &&
323 !JS_DefineFunctions(cx, holder, properties.mChromeOnlyMethods))) {
324 return false;
327 holderVal.setObject(*holder);
328 { // Scope for working with the map
329 JSAutoRealm ar(cx, map);
331 // Key is already in the right Realm, but we need to wrap the value.
332 if (!MaybeWrapValue(cx, &holderVal)) {
333 return false;
336 if (!JS::SetWeakMapEntry(cx, map, key, holderVal)) {
337 return false;
341 return true;
344 /* static */
345 bool MaybeCrossOriginObjectMixins::ReportCrossOriginDenial(
346 JSContext* aCx, JS::Handle<jsid> aId, const nsACString& aAccessType) {
347 xpc::AccessCheck::reportCrossOriginDenial(aCx, aId, aAccessType);
348 return false;
351 template <typename Base>
352 bool MaybeCrossOriginObject<Base>::getPrototype(
353 JSContext* cx, JS::Handle<JSObject*> proxy,
354 JS::MutableHandle<JSObject*> protop) const {
355 if (!IsPlatformObjectSameOrigin(cx, proxy)) {
356 protop.set(nullptr);
357 return true;
360 { // Scope for JSAutoRealm
361 JSAutoRealm ar(cx, proxy);
362 protop.set(getSameOriginPrototype(cx));
363 if (!protop) {
364 return false;
368 return MaybeWrapObject(cx, protop);
371 template <typename Base>
372 bool MaybeCrossOriginObject<Base>::setPrototype(
373 JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<JSObject*> proto,
374 JS::ObjectOpResult& result) const {
375 // Inlined version of
376 // https://tc39.github.io/ecma262/#sec-set-immutable-prototype
377 js::AssertSameCompartment(cx, proto);
379 // We have to be careful how we get the prototype. In particular, we do _NOT_
380 // want to enter the Realm of "proxy" to do that, in case we're not
381 // same-origin with it here.
382 JS::Rooted<JSObject*> wrappedProxy(cx, proxy);
383 if (!MaybeWrapObject(cx, &wrappedProxy)) {
384 return false;
387 JS::Rooted<JSObject*> currentProto(cx);
388 if (!js::GetObjectProto(cx, wrappedProxy, &currentProto)) {
389 return false;
392 if (currentProto != proto) {
393 return result.failCantSetProto();
396 return result.succeed();
399 template <typename Base>
400 bool MaybeCrossOriginObject<Base>::getPrototypeIfOrdinary(
401 JSContext* cx, JS::Handle<JSObject*> proxy, bool* isOrdinary,
402 JS::MutableHandle<JSObject*> protop) const {
403 // We have a custom [[GetPrototypeOf]]
404 *isOrdinary = false;
405 return true;
408 template <typename Base>
409 bool MaybeCrossOriginObject<Base>::setImmutablePrototype(
410 JSContext* cx, JS::Handle<JSObject*> proxy, bool* succeeded) const {
411 // We just want to disallow this.
412 *succeeded = false;
413 return true;
416 template <typename Base>
417 bool MaybeCrossOriginObject<Base>::isExtensible(JSContext* cx,
418 JS::Handle<JSObject*> proxy,
419 bool* extensible) const {
420 // We never allow [[PreventExtensions]] to succeed.
421 *extensible = true;
422 return true;
425 template <typename Base>
426 bool MaybeCrossOriginObject<Base>::preventExtensions(
427 JSContext* cx, JS::Handle<JSObject*> proxy,
428 JS::ObjectOpResult& result) const {
429 return result.failCantPreventExtensions();
432 template <typename Base>
433 bool MaybeCrossOriginObject<Base>::defineProperty(
434 JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
435 JS::Handle<JS::PropertyDescriptor> desc, JS::ObjectOpResult& result) const {
436 if (!IsPlatformObjectSameOrigin(cx, proxy)) {
437 return ReportCrossOriginDenial(cx, id, "define"_ns);
440 // Enter the Realm of proxy and do the remaining work in there.
441 JSAutoRealm ar(cx, proxy);
442 JS::Rooted<JS::PropertyDescriptor> descCopy(cx, desc);
443 if (!JS_WrapPropertyDescriptor(cx, &descCopy)) {
444 return false;
447 JS_MarkCrossZoneId(cx, id);
449 return definePropertySameOrigin(cx, proxy, id, descCopy, result);
452 template <typename Base>
453 bool MaybeCrossOriginObject<Base>::enumerate(
454 JSContext* cx, JS::Handle<JSObject*> proxy,
455 JS::MutableHandleVector<jsid> props) const {
456 // Just get the property keys from ourselves, in whatever Realm we happen to
457 // be in. It's important to not enter the Realm of "proxy" here, because that
458 // would affect the list of keys we claim to have. We wrap the proxy in the
459 // current compartment just to be safe; it doesn't affect behavior as far as
460 // CrossOriginObjectWrapper and MaybeCrossOriginObject are concerned.
461 JS::Rooted<JSObject*> self(cx, proxy);
462 if (!MaybeWrapObject(cx, &self)) {
463 return false;
466 return js::GetPropertyKeys(cx, self, 0, props);
469 template <typename Base>
470 bool MaybeCrossOriginObject<Base>::hasInstance(JSContext* cx,
471 JS::Handle<JSObject*> proxy,
472 JS::MutableHandle<JS::Value> v,
473 bool* bp) const {
474 if (!IsPlatformObjectSameOrigin(cx, proxy)) {
475 // In the cross-origin case we never have @@hasInstance, and we're never
476 // callable, so just go ahead and report an error. If we enter the realm of
477 // "proxy" to do that, our caller won't be able to do anything with the
478 // exception, so instead let's wrap "proxy" into our realm. We definitely
479 // do NOT want to call JS::InstanceofOperator here after entering "proxy's"
480 // realm, because that would do the wrong thing with @@hasInstance on the
481 // object by seeing any such definitions when we should not.
482 JS::Rooted<JS::Value> val(cx, JS::ObjectValue(*proxy));
483 if (!MaybeWrapValue(cx, &val)) {
484 return false;
486 return js::ReportIsNotFunction(cx, val);
489 // We need to wrap `proxy` into our compartment or enter proxy's realm
490 // and wrap `v` into proxy's compartment because at this point `v` and `proxy`
491 // might no longer be same-compartment. One solution is to enter the realm of
492 // `proxy` and look up @@hasInstance there. However, that will lead to
493 // incorrect error reporting because the mechanism for reporting the "not a
494 // function" exception only works correctly if we are in the realm of the
495 // script that encountered the instanceof expression. Thus, we don't want to
496 // switch realms and will wrap `proxy` into our current compartment and lookup
497 // @@hasInstance. Note that accesses to get @@hasInstance on `proxy` after it
498 // is wrapped in the `cx` compartment will still work because `cx` and `proxy`
499 // are same-origin.
500 JS::Rooted<JSObject*> proxyWrap(cx, proxy);
501 if (!MaybeWrapObject(cx, &proxyWrap)) {
502 return false;
504 // We are not calling BaseProxyHandler::hasInstance here because it expects
505 // `proxy` to be passed as the object. However, `proxy`, as a
506 // MaybeCrossOriginObject, may not be in current cx->realm() and we may now
507 // have a cross-compartment wrapper for `proxy`.
508 return JS::InstanceofOperator(cx, proxyWrap, v, bp);
511 // Force instantiations of the out-of-line template methods we need.
512 template class MaybeCrossOriginObject<js::Wrapper>;
513 template class MaybeCrossOriginObject<DOMProxyHandler>;
515 } // namespace dom
516 } // namespace mozilla