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/ProxyHandlerUtils.h"
10 #include "mozilla/dom/WindowBinding.h"
11 #include "mozilla/dom/WindowProxyHolder.h"
12 #include "nsContentUtils.h"
13 #include "nsGlobalWindowInner.h"
14 #include "nsGlobalWindowOuter.h"
15 #include "nsHTMLDocument.h"
16 #include "nsJSUtils.h"
17 #include "xpcprivate.h"
19 namespace mozilla::dom
{
21 static bool ShouldExposeChildWindow(const nsString
& aNameBeingResolved
,
22 BrowsingContext
* aChild
) {
23 Element
* e
= aChild
->GetEmbedderElement();
24 if (e
&& e
->IsInShadowTree()) {
28 // If we're same-origin with the child, go ahead and expose it.
29 nsPIDOMWindowOuter
* child
= aChild
->GetDOMWindow();
30 nsCOMPtr
<nsIScriptObjectPrincipal
> sop
= do_QueryInterface(child
);
31 if (sop
&& nsContentUtils::SubjectPrincipal()->Equals(sop
->GetPrincipal())) {
35 // If we're not same-origin, expose it _only_ if the name of the browsing
36 // context matches the 'name' attribute of the frame element in the parent.
37 // The motivations behind this heuristic are worth explaining here.
39 // Historically, all UAs supported global named access to any child browsing
40 // context (that is to say, window.dolske returns a child frame where either
41 // the "name" attribute on the frame element was set to "dolske", or where
42 // the child explicitly set window.name = "dolske").
44 // This is problematic because it allows possibly-malicious and unrelated
45 // cross-origin subframes to pollute the global namespace of their parent in
46 // unpredictable ways (see bug 860494). This is also problematic for browser
47 // engines like Servo that want to run cross-origin script on different
50 // The naive solution here would be to filter out any cross-origin subframes
51 // obtained when doing named lookup in global scope. But that is unlikely to
52 // be web-compatible, since it will break named access for consumers that do
53 // <iframe name="dolske" src="http://cross-origin.com/sadtrombone.html"> and
54 // expect to be able to access the cross-origin subframe via named lookup on
57 // The optimal behavior would be to do the following:
58 // (a) Look for any child browsing context with name="dolske".
59 // (b) If the result is cross-origin, null it out.
60 // (c) If we have null, look for a frame element whose 'name' attribute is
63 // Unfortunately, (c) would require some engineering effort to be performant
64 // in Gecko, and probably in other UAs as well. So we go with a simpler
65 // approximation of the above. This approximation will only break sites that
66 // rely on their cross-origin subframes setting window.name to a known value,
67 // which is unlikely to be very common. And while it does introduce a
68 // dependency on cross-origin state when doing global lookups, it doesn't
69 // allow the child to arbitrarily pollute the parent namespace, and requires
70 // cross-origin communication only in a limited set of cases that can be
71 // computed independently by the parent.
72 return e
&& e
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::name
,
73 aNameBeingResolved
, eCaseMatters
);
76 bool WindowNamedPropertiesHandler::getOwnPropDescriptor(
77 JSContext
* aCx
, JS::Handle
<JSObject
*> aProxy
, JS::Handle
<jsid
> aId
,
79 JS::MutableHandle
<Maybe
<JS::PropertyDescriptor
>> aDesc
) const {
83 if (aId
.isWellKnownSymbol(JS::SymbolCode::toStringTag
)) {
84 JS::Rooted
<JSString
*> toStringTagStr(
85 aCx
, JS_NewStringCopyZ(aCx
, "WindowProperties"));
86 if (!toStringTagStr
) {
91 JS::PropertyDescriptor::Data(JS::StringValue(toStringTagStr
),
92 {JS::PropertyAttribute::Configurable
})));
96 // Nothing to do if we're resolving another symbol property.
101 if (!HasPropertyOnPrototype(aCx
, aProxy
, aId
, &hasOnPrototype
)) {
104 if (hasOnPrototype
) {
109 if (!str
.init(aCx
, aId
)) {
117 // Grab the DOM window.
118 nsGlobalWindowInner
* win
= xpc::WindowGlobalOrNull(aProxy
);
119 if (win
->Length() > 0) {
120 RefPtr
<BrowsingContext
> child
= win
->GetChildWindow(str
);
121 if (child
&& ShouldExposeChildWindow(str
, child
)) {
122 // We found a subframe of the right name. Shadowing via |var foo| in
123 // global scope is still allowed, since |var| only looks up |own|
124 // properties. But unqualified shadowing will fail, per-spec.
125 JS::Rooted
<JS::Value
> v(aCx
);
126 if (!ToJSValue(aCx
, WindowProxyHolder(std::move(child
)), &v
)) {
129 aDesc
.set(mozilla::Some(
130 JS::PropertyDescriptor::Data(v
, {JS::PropertyAttribute::Configurable
,
131 JS::PropertyAttribute::Writable
})));
136 // The rest of this function is for HTML documents only.
137 Document
* doc
= win
->GetExtantDoc();
138 if (!doc
|| !doc
->IsHTMLOrXHTML()) {
141 nsHTMLDocument
* document
= doc
->AsHTMLDocument();
143 JS::Rooted
<JS::Value
> v(aCx
);
144 Element
* element
= document
->GetElementById(str
);
146 if (!ToJSValue(aCx
, element
, &v
)) {
149 aDesc
.set(mozilla::Some(
150 JS::PropertyDescriptor::Data(v
, {JS::PropertyAttribute::Configurable
,
151 JS::PropertyAttribute::Writable
})));
156 bool found
= document
->ResolveName(aCx
, str
, &v
, rv
);
157 if (rv
.MaybeSetPendingException(aCx
)) {
162 aDesc
.set(mozilla::Some(
163 JS::PropertyDescriptor::Data(v
, {JS::PropertyAttribute::Configurable
,
164 JS::PropertyAttribute::Writable
})));
169 bool WindowNamedPropertiesHandler::defineProperty(
170 JSContext
* aCx
, JS::Handle
<JSObject
*> aProxy
, JS::Handle
<jsid
> aId
,
171 JS::Handle
<JS::PropertyDescriptor
> aDesc
,
172 JS::ObjectOpResult
& result
) const {
173 return result
.failCantDefineWindowNamedProperty();
176 bool WindowNamedPropertiesHandler::ownPropNames(
177 JSContext
* aCx
, JS::Handle
<JSObject
*> aProxy
, unsigned flags
,
178 JS::MutableHandleVector
<jsid
> aProps
) const {
179 if (!(flags
& JSITER_HIDDEN
)) {
180 // None of our named properties are enumerable.
184 // Grab the DOM window.
185 nsGlobalWindowInner
* win
= xpc::WindowGlobalOrNull(aProxy
);
186 nsTArray
<nsString
> names
;
187 // The names live on the outer window, which might be null
188 nsGlobalWindowOuter
* outer
= win
->GetOuterWindowInternal();
190 if (BrowsingContext
* bc
= outer
->GetBrowsingContext()) {
191 for (const auto& child
: bc
->Children()) {
192 const nsString
& name
= child
->Name();
193 if (!name
.IsEmpty() && !names
.Contains(name
)) {
194 // Make sure we really would expose it from getOwnPropDescriptor.
195 if (ShouldExposeChildWindow(name
, child
)) {
196 names
.AppendElement(name
);
202 if (!AppendNamedPropertyIds(aCx
, aProxy
, names
, false, aProps
)) {
207 Document
* doc
= win
->GetExtantDoc();
208 if (!doc
|| !doc
->IsHTMLOrXHTML()) {
209 // Define to @@toStringTag on this object to keep Object.prototype.toString
210 // backwards compatible.
211 JS::Rooted
<jsid
> toStringTagId(
212 aCx
, JS::GetWellKnownSymbolKey(aCx
, JS::SymbolCode::toStringTag
));
213 return aProps
.append(toStringTagId
);
216 nsHTMLDocument
* document
= doc
->AsHTMLDocument();
217 // Document names are enumerable, so we want to get them no matter what flags
219 document
->GetSupportedNames(names
);
221 JS::RootedVector
<jsid
> docProps(aCx
);
222 if (!AppendNamedPropertyIds(aCx
, aProxy
, names
, false, &docProps
)) {
226 JS::Rooted
<jsid
> toStringTagId(
227 aCx
, JS::GetWellKnownSymbolKey(aCx
, JS::SymbolCode::toStringTag
));
228 if (!docProps
.append(toStringTagId
)) {
232 return js::AppendUnique(aCx
, aProps
, docProps
);
235 bool WindowNamedPropertiesHandler::delete_(JSContext
* aCx
,
236 JS::Handle
<JSObject
*> aProxy
,
237 JS::Handle
<jsid
> aId
,
238 JS::ObjectOpResult
& aResult
) const {
239 return aResult
.failCantDeleteWindowNamedProperty();
242 // Note that this class doesn't need any reserved slots, but SpiderMonkey
243 // asserts all proxy classes have at least one reserved slot.
244 static const DOMIfaceAndProtoJSClass WindowNamedPropertiesClass
= {
245 PROXY_CLASS_DEF("WindowProperties", JSCLASS_IS_DOMIFACEANDPROTOJSCLASS
|
246 JSCLASS_HAS_RESERVED_SLOTS(1)),
247 eNamedPropertiesObject
,
248 prototypes::id::_ID_Count
,
250 &sEmptyNativePropertyHooks
,
251 EventTarget_Binding::GetProtoObject
};
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
));
267 if (!JS_SetImmutablePrototype(aCx
, gsp
, &succeeded
)) {
270 MOZ_ASSERT(succeeded
,
271 "errors making the [[Prototype]] of the named properties object "
272 "immutable should have been JSAPI failures, not !succeeded");
277 } // namespace mozilla::dom