WindowNamedPropertiesHandler.cpp (10231B)
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 "WindowNamedPropertiesHandler.h" 8 9 #include "mozilla/dom/EventTargetBinding.h" 10 #include "mozilla/dom/ProxyHandlerUtils.h" 11 #include "mozilla/dom/WindowBinding.h" 12 #include "mozilla/dom/WindowProxyHolder.h" 13 #include "nsContentUtils.h" 14 #include "nsGlobalWindowInner.h" 15 #include "nsGlobalWindowOuter.h" 16 #include "nsHTMLDocument.h" 17 #include "nsJSUtils.h" 18 #include "xpcprivate.h" 19 20 namespace mozilla::dom { 21 22 static bool ShouldExposeChildWindow(const nsString& aNameBeingResolved, 23 BrowsingContext* aChild) { 24 Element* e = aChild->GetEmbedderElement(); 25 if (e && e->IsInShadowTree()) { 26 return false; 27 } 28 29 // If we're same-origin with the child, go ahead and expose it. 30 nsPIDOMWindowOuter* child = aChild->GetDOMWindow(); 31 nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(child); 32 if (sop && nsContentUtils::SubjectPrincipal()->Equals(sop->GetPrincipal())) { 33 return true; 34 } 35 36 // If we're not same-origin, expose it _only_ if the name of the browsing 37 // context matches the 'name' attribute of the frame element in the parent. 38 // The motivations behind this heuristic are worth explaining here. 39 // 40 // Historically, all UAs supported global named access to any child browsing 41 // context (that is to say, window.dolske returns a child frame where either 42 // the "name" attribute on the frame element was set to "dolske", or where 43 // the child explicitly set window.name = "dolske"). 44 // 45 // This is problematic because it allows possibly-malicious and unrelated 46 // cross-origin subframes to pollute the global namespace of their parent in 47 // unpredictable ways (see bug 860494). This is also problematic for browser 48 // engines like Servo that want to run cross-origin script on different 49 // threads. 50 // 51 // The naive solution here would be to filter out any cross-origin subframes 52 // obtained when doing named lookup in global scope. But that is unlikely to 53 // be web-compatible, since it will break named access for consumers that do 54 // <iframe name="dolske" src="http://cross-origin.com/sadtrombone.html"> and 55 // expect to be able to access the cross-origin subframe via named lookup on 56 // the global. 57 // 58 // The optimal behavior would be to do the following: 59 // (a) Look for any child browsing context with name="dolske". 60 // (b) If the result is cross-origin, null it out. 61 // (c) If we have null, look for a frame element whose 'name' attribute is 62 // "dolske". 63 // 64 // Unfortunately, (c) would require some engineering effort to be performant 65 // in Gecko, and probably in other UAs as well. So we go with a simpler 66 // approximation of the above. This approximation will only break sites that 67 // rely on their cross-origin subframes setting window.name to a known value, 68 // which is unlikely to be very common. And while it does introduce a 69 // dependency on cross-origin state when doing global lookups, it doesn't 70 // allow the child to arbitrarily pollute the parent namespace, and requires 71 // cross-origin communication only in a limited set of cases that can be 72 // computed independently by the parent. 73 return e && e->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, 74 aNameBeingResolved, eCaseMatters); 75 } 76 77 bool WindowNamedPropertiesHandler::getOwnPropDescriptor( 78 JSContext* aCx, JS::Handle<JSObject*> aProxy, JS::Handle<jsid> aId, 79 bool /* unused */, 80 JS::MutableHandle<Maybe<JS::PropertyDescriptor>> aDesc) const { 81 aDesc.reset(); 82 83 if (aId.isSymbol()) { 84 if (aId.isWellKnownSymbol(JS::SymbolCode::toStringTag)) { 85 JS::Rooted<JSString*> toStringTagStr( 86 aCx, JS_NewStringCopyZ(aCx, "WindowProperties")); 87 if (!toStringTagStr) { 88 return false; 89 } 90 91 aDesc.set(Some( 92 JS::PropertyDescriptor::Data(JS::StringValue(toStringTagStr), 93 {JS::PropertyAttribute::Configurable}))); 94 return true; 95 } 96 97 // Nothing to do if we're resolving another symbol property. 98 return true; 99 } 100 101 bool hasOnPrototype; 102 if (!HasPropertyOnPrototype(aCx, aProxy, aId, &hasOnPrototype)) { 103 return false; 104 } 105 if (hasOnPrototype) { 106 return true; 107 } 108 109 nsAutoJSString str; 110 if (!str.init(aCx, aId)) { 111 return false; 112 } 113 114 if (str.IsEmpty()) { 115 return true; 116 } 117 118 // Grab the DOM window. 119 nsGlobalWindowInner* win = xpc::WindowGlobalOrNull(aProxy); 120 if (win->Length() > 0) { 121 RefPtr<BrowsingContext> child = win->GetChildWindow(str); 122 if (child && ShouldExposeChildWindow(str, child)) { 123 // We found a subframe of the right name. Shadowing via |var foo| in 124 // global scope is still allowed, since |var| only looks up |own| 125 // properties. But unqualified shadowing will fail, per-spec. 126 JS::Rooted<JS::Value> v(aCx); 127 if (!ToJSValue(aCx, WindowProxyHolder(std::move(child)), &v)) { 128 return false; 129 } 130 aDesc.set(mozilla::Some( 131 JS::PropertyDescriptor::Data(v, {JS::PropertyAttribute::Configurable, 132 JS::PropertyAttribute::Writable}))); 133 return true; 134 } 135 } 136 137 // The rest of this function is for HTML documents only. 138 Document* doc = win->GetExtantDoc(); 139 if (!doc || !doc->IsHTMLOrXHTML()) { 140 return true; 141 } 142 nsHTMLDocument* document = doc->AsHTMLDocument(); 143 144 JS::Rooted<JS::Value> v(aCx); 145 Element* element = document->GetElementById(str); 146 if (element) { 147 if (!ToJSValue(aCx, element, &v)) { 148 return false; 149 } 150 aDesc.set(mozilla::Some( 151 JS::PropertyDescriptor::Data(v, {JS::PropertyAttribute::Configurable, 152 JS::PropertyAttribute::Writable}))); 153 return true; 154 } 155 156 ErrorResult rv; 157 bool found = document->ResolveNameForWindow(aCx, str, &v, rv); 158 if (rv.MaybeSetPendingException(aCx)) { 159 return false; 160 } 161 162 if (found) { 163 aDesc.set(mozilla::Some( 164 JS::PropertyDescriptor::Data(v, {JS::PropertyAttribute::Configurable, 165 JS::PropertyAttribute::Writable}))); 166 } 167 return true; 168 } 169 170 bool WindowNamedPropertiesHandler::defineProperty( 171 JSContext* aCx, JS::Handle<JSObject*> aProxy, JS::Handle<jsid> aId, 172 JS::Handle<JS::PropertyDescriptor> aDesc, 173 JS::ObjectOpResult& result) const { 174 return result.failCantDefineWindowNamedProperty(); 175 } 176 177 bool WindowNamedPropertiesHandler::ownPropNames( 178 JSContext* aCx, JS::Handle<JSObject*> aProxy, unsigned flags, 179 JS::MutableHandleVector<jsid> aProps) const { 180 if (!(flags & JSITER_HIDDEN)) { 181 // None of our named properties are enumerable. 182 return true; 183 } 184 185 // Grab the DOM window. 186 nsGlobalWindowInner* win = xpc::WindowGlobalOrNull(aProxy); 187 nsTArray<nsString> names; 188 // The names live on the outer window, which might be null 189 nsGlobalWindowOuter* outer = win->GetOuterWindowInternal(); 190 if (outer) { 191 if (BrowsingContext* bc = outer->GetBrowsingContext()) { 192 for (const auto& child : bc->Children()) { 193 const nsString& name = child->Name(); 194 if (!name.IsEmpty() && !names.Contains(name)) { 195 // Make sure we really would expose it from getOwnPropDescriptor. 196 if (ShouldExposeChildWindow(name, child)) { 197 names.AppendElement(name); 198 } 199 } 200 } 201 } 202 } 203 if (!AppendNamedPropertyIds(aCx, aProxy, names, false, aProps)) { 204 return false; 205 } 206 207 names.Clear(); 208 Document* doc = win->GetExtantDoc(); 209 if (!doc || !doc->IsHTMLOrXHTML()) { 210 // Define to @@toStringTag on this object to keep Object.prototype.toString 211 // backwards compatible. 212 JS::Rooted<jsid> toStringTagId( 213 aCx, JS::GetWellKnownSymbolKey(aCx, JS::SymbolCode::toStringTag)); 214 return aProps.append(toStringTagId); 215 } 216 217 nsHTMLDocument* document = doc->AsHTMLDocument(); 218 // Document names are enumerable, so we want to get them no matter what flags 219 // is. 220 document->GetSupportedNamesForWindow(names); 221 222 JS::RootedVector<jsid> docProps(aCx); 223 if (!AppendNamedPropertyIds(aCx, aProxy, names, false, &docProps)) { 224 return false; 225 } 226 227 JS::Rooted<jsid> toStringTagId( 228 aCx, JS::GetWellKnownSymbolKey(aCx, JS::SymbolCode::toStringTag)); 229 if (!docProps.append(toStringTagId)) { 230 return false; 231 } 232 233 return js::AppendUnique(aCx, aProps, docProps); 234 } 235 236 bool WindowNamedPropertiesHandler::delete_(JSContext* aCx, 237 JS::Handle<JSObject*> aProxy, 238 JS::Handle<jsid> aId, 239 JS::ObjectOpResult& aResult) const { 240 return aResult.failCantDeleteWindowNamedProperty(); 241 } 242 243 // Note that this class doesn't need any reserved slots, but SpiderMonkey 244 // asserts all proxy classes have at least one reserved slot. 245 static const DOMIfaceAndProtoJSClass WindowNamedPropertiesClass = { 246 PROXY_CLASS_DEF("WindowProperties", JSCLASS_IS_DOMIFACEANDPROTOJSCLASS | 247 JSCLASS_HAS_RESERVED_SLOTS(1)), 248 eNamedPropertiesObject, 249 prototypes::id::_ID_Count, 250 0, 251 &sEmptyNativePropertyHooks, 252 EventTarget_Binding::GetProtoObject}; 253 254 // static 255 JSObject* WindowNamedPropertiesHandler::Create(JSContext* aCx, 256 JS::Handle<JSObject*> aProto) { 257 js::ProxyOptions options; 258 options.setClass(&WindowNamedPropertiesClass.mBase); 259 260 JS::Rooted<JSObject*> gsp( 261 aCx, js::NewProxyObject(aCx, WindowNamedPropertiesHandler::getInstance(), 262 JS::NullHandleValue, aProto, options)); 263 if (!gsp) { 264 return nullptr; 265 } 266 267 bool succeeded; 268 if (!JS_SetImmutablePrototype(aCx, gsp, &succeeded)) { 269 return nullptr; 270 } 271 MOZ_ASSERT(succeeded, 272 "errors making the [[Prototype]] of the named properties object " 273 "immutable should have been JSAPI failures, not !succeeded"); 274 275 return gsp; 276 } 277 278 } // namespace mozilla::dom