|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
|
2 /* vim: set ts=8 sts=4 et sw=4 tw=99: */ |
|
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 "AccessCheck.h" |
|
8 |
|
9 #include "nsJSPrincipals.h" |
|
10 #include "nsGlobalWindow.h" |
|
11 |
|
12 #include "XPCWrapper.h" |
|
13 #include "XrayWrapper.h" |
|
14 |
|
15 #include "jsfriendapi.h" |
|
16 #include "mozilla/dom/BindingUtils.h" |
|
17 #include "mozilla/dom/WindowBinding.h" |
|
18 #include "nsIDOMWindowCollection.h" |
|
19 #include "nsJSUtils.h" |
|
20 |
|
21 using namespace mozilla; |
|
22 using namespace JS; |
|
23 using namespace js; |
|
24 |
|
25 namespace xpc { |
|
26 |
|
27 nsIPrincipal * |
|
28 GetCompartmentPrincipal(JSCompartment *compartment) |
|
29 { |
|
30 return nsJSPrincipals::get(JS_GetCompartmentPrincipals(compartment)); |
|
31 } |
|
32 |
|
33 nsIPrincipal * |
|
34 GetObjectPrincipal(JSObject *obj) |
|
35 { |
|
36 return GetCompartmentPrincipal(js::GetObjectCompartment(obj)); |
|
37 } |
|
38 |
|
39 // Does the principal of compartment a subsume the principal of compartment b? |
|
40 bool |
|
41 AccessCheck::subsumes(JSCompartment *a, JSCompartment *b) |
|
42 { |
|
43 nsIPrincipal *aprin = GetCompartmentPrincipal(a); |
|
44 nsIPrincipal *bprin = GetCompartmentPrincipal(b); |
|
45 return aprin->Subsumes(bprin); |
|
46 } |
|
47 |
|
48 bool |
|
49 AccessCheck::subsumes(JSObject *a, JSObject *b) |
|
50 { |
|
51 return subsumes(js::GetObjectCompartment(a), js::GetObjectCompartment(b)); |
|
52 } |
|
53 |
|
54 // Same as above, but considering document.domain. |
|
55 bool |
|
56 AccessCheck::subsumesConsideringDomain(JSCompartment *a, JSCompartment *b) |
|
57 { |
|
58 nsIPrincipal *aprin = GetCompartmentPrincipal(a); |
|
59 nsIPrincipal *bprin = GetCompartmentPrincipal(b); |
|
60 return aprin->SubsumesConsideringDomain(bprin); |
|
61 } |
|
62 |
|
63 // Does the compartment of the wrapper subsumes the compartment of the wrappee? |
|
64 bool |
|
65 AccessCheck::wrapperSubsumes(JSObject *wrapper) |
|
66 { |
|
67 MOZ_ASSERT(js::IsWrapper(wrapper)); |
|
68 JSObject *wrapped = js::UncheckedUnwrap(wrapper); |
|
69 return AccessCheck::subsumes(js::GetObjectCompartment(wrapper), |
|
70 js::GetObjectCompartment(wrapped)); |
|
71 } |
|
72 |
|
73 bool |
|
74 AccessCheck::isChrome(JSCompartment *compartment) |
|
75 { |
|
76 nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager(); |
|
77 if (!ssm) { |
|
78 return false; |
|
79 } |
|
80 |
|
81 bool privileged; |
|
82 nsIPrincipal *principal = GetCompartmentPrincipal(compartment); |
|
83 return NS_SUCCEEDED(ssm->IsSystemPrincipal(principal, &privileged)) && privileged; |
|
84 } |
|
85 |
|
86 bool |
|
87 AccessCheck::isChrome(JSObject *obj) |
|
88 { |
|
89 return isChrome(js::GetObjectCompartment(obj)); |
|
90 } |
|
91 |
|
92 bool |
|
93 AccessCheck::callerIsChrome() |
|
94 { |
|
95 nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager(); |
|
96 if (!ssm) |
|
97 return false; |
|
98 bool subjectIsSystem; |
|
99 nsresult rv = ssm->SubjectPrincipalIsSystem(&subjectIsSystem); |
|
100 return NS_SUCCEEDED(rv) && subjectIsSystem; |
|
101 } |
|
102 |
|
103 nsIPrincipal * |
|
104 AccessCheck::getPrincipal(JSCompartment *compartment) |
|
105 { |
|
106 return GetCompartmentPrincipal(compartment); |
|
107 } |
|
108 |
|
109 #define NAME(ch, str, cases) \ |
|
110 case ch: if (!strcmp(name, str)) switch (propChars[0]) { cases }; break; |
|
111 #define PROP(ch, actions) case ch: { actions }; break; |
|
112 #define RW(str) if (JS_FlatStringEqualsAscii(prop, str)) return true; |
|
113 #define R(str) if (!set && JS_FlatStringEqualsAscii(prop, str)) return true; |
|
114 #define W(str) if (set && JS_FlatStringEqualsAscii(prop, str)) return true; |
|
115 |
|
116 // Hardcoded policy for cross origin property access. This was culled from the |
|
117 // preferences file (all.js). We don't want users to overwrite highly sensitive |
|
118 // security policies. |
|
119 static bool |
|
120 IsPermitted(const char *name, JSFlatString *prop, bool set) |
|
121 { |
|
122 size_t propLength; |
|
123 const jschar *propChars = |
|
124 JS_GetInternedStringCharsAndLength(JS_FORGET_STRING_FLATNESS(prop), &propLength); |
|
125 if (!propLength) |
|
126 return false; |
|
127 switch (name[0]) { |
|
128 NAME('L', "Location", |
|
129 PROP('h', W("href")) |
|
130 PROP('r', R("replace"))) |
|
131 case 'W': |
|
132 if (!strcmp(name, "Window")) |
|
133 return dom::WindowBinding::IsPermitted(prop, propChars[0], set); |
|
134 break; |
|
135 } |
|
136 return false; |
|
137 } |
|
138 |
|
139 #undef NAME |
|
140 #undef RW |
|
141 #undef R |
|
142 #undef W |
|
143 |
|
144 static bool |
|
145 IsFrameId(JSContext *cx, JSObject *objArg, jsid idArg) |
|
146 { |
|
147 RootedObject obj(cx, objArg); |
|
148 RootedId id(cx, idArg); |
|
149 |
|
150 obj = JS_ObjectToInnerObject(cx, obj); |
|
151 MOZ_ASSERT(!js::IsWrapper(obj)); |
|
152 nsGlobalWindow* win = WindowOrNull(obj); |
|
153 if (!win) { |
|
154 return false; |
|
155 } |
|
156 |
|
157 nsCOMPtr<nsIDOMWindowCollection> col; |
|
158 win->GetFrames(getter_AddRefs(col)); |
|
159 if (!col) { |
|
160 return false; |
|
161 } |
|
162 |
|
163 nsCOMPtr<nsIDOMWindow> domwin; |
|
164 if (JSID_IS_INT(id)) { |
|
165 col->Item(JSID_TO_INT(id), getter_AddRefs(domwin)); |
|
166 } else if (JSID_IS_STRING(id)) { |
|
167 col->NamedItem(nsDependentJSString(id), getter_AddRefs(domwin)); |
|
168 } |
|
169 |
|
170 return domwin != nullptr; |
|
171 } |
|
172 |
|
173 static bool |
|
174 IsWindow(const char *name) |
|
175 { |
|
176 return name[0] == 'W' && !strcmp(name, "Window"); |
|
177 } |
|
178 |
|
179 bool |
|
180 AccessCheck::isCrossOriginAccessPermitted(JSContext *cx, JSObject *wrapperArg, jsid idArg, |
|
181 Wrapper::Action act) |
|
182 { |
|
183 if (!XPCWrapper::GetSecurityManager()) |
|
184 return true; |
|
185 |
|
186 if (act == Wrapper::CALL) |
|
187 return false; |
|
188 |
|
189 RootedId id(cx, idArg); |
|
190 RootedObject wrapper(cx, wrapperArg); |
|
191 RootedObject obj(cx, Wrapper::wrappedObject(wrapper)); |
|
192 |
|
193 // For XOWs, we generally want to deny enumerate-like operations, but fail |
|
194 // silently (see CrossOriginAccessiblePropertiesOnly::deny). |
|
195 if (act == Wrapper::ENUMERATE) |
|
196 return false; |
|
197 |
|
198 const char *name; |
|
199 const js::Class *clasp = js::GetObjectClass(obj); |
|
200 MOZ_ASSERT(!XrayUtils::IsXPCWNHolderClass(Jsvalify(clasp)), "shouldn't have a holder here"); |
|
201 if (clasp->ext.innerObject) |
|
202 name = "Window"; |
|
203 else |
|
204 name = clasp->name; |
|
205 |
|
206 if (JSID_IS_STRING(id)) { |
|
207 if (IsPermitted(name, JSID_TO_FLAT_STRING(id), act == Wrapper::SET)) |
|
208 return true; |
|
209 } |
|
210 |
|
211 if (act != Wrapper::GET) |
|
212 return false; |
|
213 |
|
214 // Check for frame IDs. If we're resolving named frames, make sure to only |
|
215 // resolve ones that don't shadow native properties. See bug 860494. |
|
216 if (IsWindow(name)) { |
|
217 if (JSID_IS_STRING(id) && !XrayUtils::IsXrayResolving(cx, wrapper, id)) { |
|
218 bool wouldShadow = false; |
|
219 if (!XrayUtils::HasNativeProperty(cx, wrapper, id, &wouldShadow) || |
|
220 wouldShadow) |
|
221 { |
|
222 return false; |
|
223 } |
|
224 } |
|
225 return IsFrameId(cx, obj, id); |
|
226 } |
|
227 return false; |
|
228 } |
|
229 |
|
230 enum Access { READ = (1<<0), WRITE = (1<<1), NO_ACCESS = 0 }; |
|
231 |
|
232 static void |
|
233 EnterAndThrow(JSContext *cx, JSObject *wrapper, const char *msg) |
|
234 { |
|
235 JSAutoCompartment ac(cx, wrapper); |
|
236 JS_ReportError(cx, msg); |
|
237 } |
|
238 |
|
239 bool |
|
240 ExposedPropertiesOnly::check(JSContext *cx, JSObject *wrapperArg, jsid idArg, Wrapper::Action act) |
|
241 { |
|
242 RootedObject wrapper(cx, wrapperArg); |
|
243 RootedId id(cx, idArg); |
|
244 RootedObject wrappedObject(cx, Wrapper::wrappedObject(wrapper)); |
|
245 |
|
246 if (act == Wrapper::CALL) |
|
247 return true; |
|
248 |
|
249 RootedId exposedPropsId(cx, GetRTIdByIndex(cx, XPCJSRuntime::IDX_EXPOSEDPROPS)); |
|
250 |
|
251 // We need to enter the wrappee's compartment to look at __exposedProps__, |
|
252 // but we want to be in the wrapper's compartment if we call Deny(). |
|
253 // |
|
254 // Unfortunately, |cx| can be in either compartment when we call ::check. :-( |
|
255 JSAutoCompartment ac(cx, wrappedObject); |
|
256 |
|
257 bool found = false; |
|
258 if (!JS_HasPropertyById(cx, wrappedObject, exposedPropsId, &found)) |
|
259 return false; |
|
260 |
|
261 // Always permit access to "length" and indexed properties of arrays. |
|
262 if ((JS_IsArrayObject(cx, wrappedObject) || |
|
263 JS_IsTypedArrayObject(wrappedObject)) && |
|
264 ((JSID_IS_INT(id) && JSID_TO_INT(id) >= 0) || |
|
265 (JSID_IS_STRING(id) && JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(id), "length")))) { |
|
266 return true; // Allow |
|
267 } |
|
268 |
|
269 // If no __exposedProps__ existed, deny access. |
|
270 if (!found) { |
|
271 return false; |
|
272 } |
|
273 |
|
274 if (id == JSID_VOID) |
|
275 return true; |
|
276 |
|
277 RootedValue exposedProps(cx); |
|
278 if (!JS_LookupPropertyById(cx, wrappedObject, exposedPropsId, &exposedProps)) |
|
279 return false; |
|
280 |
|
281 if (exposedProps.isNullOrUndefined()) |
|
282 return false; |
|
283 |
|
284 if (!exposedProps.isObject()) { |
|
285 EnterAndThrow(cx, wrapper, "__exposedProps__ must be undefined, null, or an Object"); |
|
286 return false; |
|
287 } |
|
288 |
|
289 RootedObject hallpass(cx, &exposedProps.toObject()); |
|
290 |
|
291 if (!AccessCheck::subsumes(js::UncheckedUnwrap(hallpass), wrappedObject)) { |
|
292 EnterAndThrow(cx, wrapper, "Invalid __exposedProps__"); |
|
293 return false; |
|
294 } |
|
295 |
|
296 Access access = NO_ACCESS; |
|
297 |
|
298 Rooted<JSPropertyDescriptor> desc(cx); |
|
299 if (!JS_GetPropertyDescriptorById(cx, hallpass, id, &desc)) { |
|
300 return false; // Error |
|
301 } |
|
302 if (!desc.object() || !desc.isEnumerable()) |
|
303 return false; |
|
304 |
|
305 if (!desc.value().isString()) { |
|
306 EnterAndThrow(cx, wrapper, "property must be a string"); |
|
307 return false; |
|
308 } |
|
309 |
|
310 JSString *str = desc.value().toString(); |
|
311 size_t length; |
|
312 const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length); |
|
313 if (!chars) |
|
314 return false; |
|
315 |
|
316 for (size_t i = 0; i < length; ++i) { |
|
317 switch (chars[i]) { |
|
318 case 'r': |
|
319 if (access & READ) { |
|
320 EnterAndThrow(cx, wrapper, "duplicate 'readable' property flag"); |
|
321 return false; |
|
322 } |
|
323 access = Access(access | READ); |
|
324 break; |
|
325 |
|
326 case 'w': |
|
327 if (access & WRITE) { |
|
328 EnterAndThrow(cx, wrapper, "duplicate 'writable' property flag"); |
|
329 return false; |
|
330 } |
|
331 access = Access(access | WRITE); |
|
332 break; |
|
333 |
|
334 default: |
|
335 EnterAndThrow(cx, wrapper, "properties can only be readable or read and writable"); |
|
336 return false; |
|
337 } |
|
338 } |
|
339 |
|
340 if (access == NO_ACCESS) { |
|
341 EnterAndThrow(cx, wrapper, "specified properties must have a permission bit set"); |
|
342 return false; |
|
343 } |
|
344 |
|
345 if ((act == Wrapper::SET && !(access & WRITE)) || |
|
346 (act != Wrapper::SET && !(access & READ))) { |
|
347 return false; |
|
348 } |
|
349 |
|
350 return true; |
|
351 } |
|
352 |
|
353 bool |
|
354 ExposedPropertiesOnly::allowNativeCall(JSContext *cx, JS::IsAcceptableThis test, |
|
355 JS::NativeImpl impl) |
|
356 { |
|
357 return js::IsTypedArrayThisCheck(test); |
|
358 } |
|
359 |
|
360 } |