Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=78: */
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/WindowBinding.h"
9 #include "nsDOMClassInfo.h"
10 #include "nsGlobalWindow.h"
11 #include "nsHTMLDocument.h"
12 #include "nsJSUtils.h"
13 #include "xpcprivate.h"
15 namespace mozilla {
16 namespace dom {
18 static bool
19 ShouldExposeChildWindow(nsString& aNameBeingResolved, nsIDOMWindow *aChild)
20 {
21 // If we're same-origin with the child, go ahead and expose it.
22 nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aChild);
23 NS_ENSURE_TRUE(sop, false);
24 if (nsContentUtils::GetSubjectPrincipal()->Equals(sop->GetPrincipal())) {
25 return true;
26 }
28 // If we're not same-origin, expose it _only_ if the name of the browsing
29 // context matches the 'name' attribute of the frame element in the parent.
30 // The motivations behind this heuristic are worth explaining here.
31 //
32 // Historically, all UAs supported global named access to any child browsing
33 // context (that is to say, window.dolske returns a child frame where either
34 // the "name" attribute on the frame element was set to "dolske", or where
35 // the child explicitly set window.name = "dolske").
36 //
37 // This is problematic because it allows possibly-malicious and unrelated
38 // cross-origin subframes to pollute the global namespace of their parent in
39 // unpredictable ways (see bug 860494). This is also problematic for browser
40 // engines like Servo that want to run cross-origin script on different
41 // threads.
42 //
43 // The naive solution here would be to filter out any cross-origin subframes
44 // obtained when doing named lookup in global scope. But that is unlikely to
45 // be web-compatible, since it will break named access for consumers that do
46 // <iframe name="dolske" src="http://cross-origin.com/sadtrombone.html"> and
47 // expect to be able to access the cross-origin subframe via named lookup on
48 // the global.
49 //
50 // The optimal behavior would be to do the following:
51 // (a) Look for any child browsing context with name="dolske".
52 // (b) If the result is cross-origin, null it out.
53 // (c) If we have null, look for a frame element whose 'name' attribute is
54 // "dolske".
55 //
56 // Unfortunately, (c) would require some engineering effort to be performant
57 // in Gecko, and probably in other UAs as well. So we go with a simpler
58 // approximation of the above. This approximation will only break sites that
59 // rely on their cross-origin subframes setting window.name to a known value,
60 // which is unlikely to be very common. And while it does introduce a
61 // dependency on cross-origin state when doing global lookups, it doesn't
62 // allow the child to arbitrarily pollute the parent namespace, and requires
63 // cross-origin communication only in a limited set of cases that can be
64 // computed independently by the parent.
65 nsCOMPtr<nsPIDOMWindow> piWin = do_QueryInterface(aChild);
66 NS_ENSURE_TRUE(piWin, false);
67 Element* e = piWin->GetFrameElementInternal();
68 return e && e->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
69 aNameBeingResolved, eCaseMatters);
70 }
72 static nsGlobalWindow*
73 GetWindowFromGlobal(JSObject* aGlobal)
74 {
75 nsGlobalWindow* win;
76 if (NS_SUCCEEDED(UNWRAP_OBJECT(Window, aGlobal, win))) {
77 return win;
78 }
79 XPCWrappedNative* wrapper = XPCWrappedNative::Get(aGlobal);
80 nsCOMPtr<nsPIDOMWindow> piWin = do_QueryWrappedNative(wrapper);
81 MOZ_ASSERT(piWin);
82 return static_cast<nsGlobalWindow*>(piWin.get());
83 }
85 bool
86 WindowNamedPropertiesHandler::getOwnPropDescriptor(JSContext* aCx,
87 JS::Handle<JSObject*> aProxy,
88 JS::Handle<jsid> aId,
89 bool /* unused */,
90 JS::MutableHandle<JSPropertyDescriptor> aDesc)
91 {
92 if (!JSID_IS_STRING(aId)) {
93 // Nothing to do if we're resolving a non-string property.
94 return true;
95 }
97 JS::Rooted<JSObject*> global(aCx, JS_GetGlobalForObject(aCx, aProxy));
98 if (HasPropertyOnPrototype(aCx, aProxy, aId)) {
99 return true;
100 }
102 nsDependentJSString str(aId);
104 // Grab the DOM window.
105 nsGlobalWindow* win = GetWindowFromGlobal(global);
106 if (win->Length() > 0) {
107 nsCOMPtr<nsIDOMWindow> childWin = win->GetChildWindow(str);
108 if (childWin && ShouldExposeChildWindow(str, childWin)) {
109 // We found a subframe of the right name. Shadowing via |var foo| in
110 // global scope is still allowed, since |var| only looks up |own|
111 // properties. But unqualified shadowing will fail, per-spec.
112 JS::Rooted<JS::Value> v(aCx);
113 if (!WrapObject(aCx, childWin, &v)) {
114 return false;
115 }
116 aDesc.object().set(aProxy);
117 aDesc.value().set(v);
118 aDesc.setAttributes(JSPROP_ENUMERATE);
119 return true;
120 }
121 }
123 // The rest of this function is for HTML documents only.
124 nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(win->GetExtantDoc());
125 if (!htmlDoc) {
126 return true;
127 }
128 nsHTMLDocument* document = static_cast<nsHTMLDocument*>(htmlDoc.get());
130 Element* element = document->GetElementById(str);
131 if (element) {
132 JS::Rooted<JS::Value> v(aCx);
133 if (!WrapObject(aCx, element, &v)) {
134 return false;
135 }
136 aDesc.object().set(aProxy);
137 aDesc.value().set(v);
138 aDesc.setAttributes(JSPROP_ENUMERATE);
139 return true;
140 }
142 nsWrapperCache* cache;
143 nsISupports* result = document->ResolveName(str, &cache);
144 if (!result) {
145 return true;
146 }
148 JS::Rooted<JS::Value> v(aCx);
149 if (!WrapObject(aCx, result, cache, nullptr, &v)) {
150 return false;
151 }
152 aDesc.object().set(aProxy);
153 aDesc.value().set(v);
154 aDesc.setAttributes(JSPROP_ENUMERATE);
155 return true;
156 }
158 bool
159 WindowNamedPropertiesHandler::defineProperty(JSContext* aCx,
160 JS::Handle<JSObject*> aProxy,
161 JS::Handle<jsid> aId,
162 JS::MutableHandle<JSPropertyDescriptor> aDesc)
163 {
164 ErrorResult rv;
165 rv.ThrowTypeError(MSG_DEFINEPROPERTY_ON_GSP);
166 rv.ReportTypeError(aCx);
167 return false;
168 }
170 bool
171 WindowNamedPropertiesHandler::ownPropNames(JSContext* aCx,
172 JS::Handle<JSObject*> aProxy,
173 unsigned flags,
174 JS::AutoIdVector& aProps)
175 {
176 // Grab the DOM window.
177 nsGlobalWindow* win = GetWindowFromGlobal(JS_GetGlobalForObject(aCx, aProxy));
178 nsTArray<nsString> names;
179 win->GetSupportedNames(names);
180 // Filter out the ones we wouldn't expose from getOwnPropertyDescriptor.
181 // We iterate backwards so we can remove things from the list easily.
182 for (size_t i = names.Length(); i > 0; ) {
183 --i; // Now we're pointing at the next name we want to look at
184 nsCOMPtr<nsIDOMWindow> childWin = win->GetChildWindow(names[i]);
185 if (!childWin || !ShouldExposeChildWindow(names[i], childWin)) {
186 names.RemoveElementAt(i);
187 }
188 }
189 if (!AppendNamedPropertyIds(aCx, aProxy, names, false, aProps)) {
190 return false;
191 }
193 names.Clear();
194 nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(win->GetExtantDoc());
195 if (!htmlDoc) {
196 return true;
197 }
198 nsHTMLDocument* document = static_cast<nsHTMLDocument*>(htmlDoc.get());
199 document->GetSupportedNames(flags, names);
201 JS::AutoIdVector docProps(aCx);
202 if (!AppendNamedPropertyIds(aCx, aProxy, names, false, docProps)) {
203 return false;
204 }
206 return js::AppendUnique(aCx, aProps, docProps);
207 }
209 bool
210 WindowNamedPropertiesHandler::delete_(JSContext* aCx,
211 JS::Handle<JSObject*> aProxy,
212 JS::Handle<jsid> aId, bool* aBp)
213 {
214 *aBp = false;
215 return true;
216 }
218 // static
219 void
220 WindowNamedPropertiesHandler::Install(JSContext* aCx,
221 JS::Handle<JSObject*> aProto)
222 {
223 JS::Rooted<JSObject*> protoProto(aCx);
224 if (!::JS_GetPrototype(aCx, aProto, &protoProto)) {
225 return;
226 }
228 // Note: since the scope polluter proxy lives on the window's prototype
229 // chain, it needs a singleton type to avoid polluting type information
230 // for properties on the window.
231 JS::Rooted<JSObject*> gsp(aCx);
232 js::ProxyOptions options;
233 options.setSingleton(true);
234 gsp = js::NewProxyObject(aCx, WindowNamedPropertiesHandler::getInstance(),
235 JS::NullHandleValue, protoProto,
236 js::GetGlobalForObjectCrossCompartment(aProto),
237 options);
238 if (!gsp) {
239 return;
240 }
242 // And then set the prototype of the interface prototype object to be the
243 // global scope polluter.
244 ::JS_SplicePrototype(aCx, aProto, gsp);
245 }
247 } // namespace dom
248 } // namespace mozilla