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