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, ¤tProto)) { 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