Line data Source code
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 "BasePrincipal.h"
11 : #include "nsGlobalWindow.h"
12 :
13 : #include "XPCWrapper.h"
14 : #include "XrayWrapper.h"
15 : #include "FilteringWrapper.h"
16 :
17 : #include "jsfriendapi.h"
18 : #include "mozilla/ErrorResult.h"
19 : #include "mozilla/dom/BindingUtils.h"
20 : #include "mozilla/dom/LocationBinding.h"
21 : #include "mozilla/dom/WindowBinding.h"
22 : #include "mozilla/jsipc/CrossProcessObjectWrappers.h"
23 : #include "nsIDOMWindowCollection.h"
24 : #include "nsJSUtils.h"
25 : #include "xpcprivate.h"
26 :
27 : using namespace mozilla;
28 : using namespace JS;
29 : using namespace js;
30 :
31 : namespace xpc {
32 :
33 : nsIPrincipal*
34 69517 : GetCompartmentPrincipal(JSCompartment* compartment)
35 : {
36 69517 : return nsJSPrincipals::get(JS_GetCompartmentPrincipals(compartment));
37 : }
38 :
39 : nsIPrincipal*
40 33 : GetObjectPrincipal(JSObject* obj)
41 : {
42 33 : return GetCompartmentPrincipal(js::GetObjectCompartment(obj));
43 : }
44 :
45 : // Does the principal of compartment a subsume the principal of compartment b?
46 : bool
47 5 : AccessCheck::subsumes(JSCompartment* a, JSCompartment* b)
48 : {
49 5 : nsIPrincipal* aprin = GetCompartmentPrincipal(a);
50 5 : nsIPrincipal* bprin = GetCompartmentPrincipal(b);
51 5 : return BasePrincipal::Cast(aprin)->FastSubsumes(bprin);
52 : }
53 :
54 : bool
55 4 : AccessCheck::subsumes(JSObject* a, JSObject* b)
56 : {
57 4 : return subsumes(js::GetObjectCompartment(a), js::GetObjectCompartment(b));
58 : }
59 :
60 : // Same as above, but considering document.domain.
61 : bool
62 18621 : AccessCheck::subsumesConsideringDomain(JSCompartment* a, JSCompartment* b)
63 : {
64 18621 : MOZ_ASSERT(OriginAttributes::IsRestrictOpenerAccessForFPI());
65 18621 : nsIPrincipal* aprin = GetCompartmentPrincipal(a);
66 18621 : nsIPrincipal* bprin = GetCompartmentPrincipal(b);
67 18621 : return BasePrincipal::Cast(aprin)->FastSubsumesConsideringDomain(bprin);
68 : }
69 :
70 : bool
71 0 : AccessCheck::subsumesConsideringDomainIgnoringFPD(JSCompartment* a,
72 : JSCompartment* b)
73 : {
74 0 : MOZ_ASSERT(!OriginAttributes::IsRestrictOpenerAccessForFPI());
75 0 : nsIPrincipal* aprin = GetCompartmentPrincipal(a);
76 0 : nsIPrincipal* bprin = GetCompartmentPrincipal(b);
77 0 : return BasePrincipal::Cast(aprin)->FastSubsumesConsideringDomainIgnoringFPD(bprin);
78 : }
79 :
80 : // Does the compartment of the wrapper subsumes the compartment of the wrappee?
81 : bool
82 0 : AccessCheck::wrapperSubsumes(JSObject* wrapper)
83 : {
84 0 : MOZ_ASSERT(js::IsWrapper(wrapper));
85 0 : JSObject* wrapped = js::UncheckedUnwrap(wrapper);
86 0 : return AccessCheck::subsumes(js::GetObjectCompartment(wrapper),
87 0 : js::GetObjectCompartment(wrapped));
88 : }
89 :
90 : bool
91 32232 : AccessCheck::isChrome(JSCompartment* compartment)
92 : {
93 32232 : nsIPrincipal* principal = GetCompartmentPrincipal(compartment);
94 32232 : return nsXPConnect::SystemPrincipal() == principal;
95 : }
96 :
97 : bool
98 3353 : AccessCheck::isChrome(JSObject* obj)
99 : {
100 3353 : return isChrome(js::GetObjectCompartment(obj));
101 : }
102 :
103 : nsIPrincipal*
104 0 : AccessCheck::getPrincipal(JSCompartment* compartment)
105 : {
106 0 : return GetCompartmentPrincipal(compartment);
107 : }
108 :
109 : // Hardcoded policy for cross origin property access. See the HTML5 Spec.
110 : static bool
111 0 : IsPermitted(CrossOriginObjectType type, JSFlatString* prop, bool set)
112 : {
113 0 : size_t propLength = JS_GetStringLength(JS_FORGET_STRING_FLATNESS(prop));
114 0 : if (!propLength)
115 0 : return false;
116 :
117 0 : char16_t propChar0 = JS_GetFlatStringCharAt(prop, 0);
118 0 : if (type == CrossOriginLocation)
119 0 : return dom::LocationBinding::IsPermitted(prop, propChar0, set);
120 0 : if (type == CrossOriginWindow)
121 0 : return dom::WindowBinding::IsPermitted(prop, propChar0, set);
122 :
123 0 : return false;
124 : }
125 :
126 : static bool
127 0 : IsFrameId(JSContext* cx, JSObject* obj, jsid idArg)
128 : {
129 0 : MOZ_ASSERT(!js::IsWrapper(obj));
130 0 : RootedId id(cx, idArg);
131 :
132 0 : nsGlobalWindow* win = WindowOrNull(obj);
133 0 : if (!win) {
134 0 : return false;
135 : }
136 :
137 0 : nsCOMPtr<nsIDOMWindowCollection> col = win->GetFrames();
138 0 : if (!col) {
139 0 : return false;
140 : }
141 :
142 0 : nsCOMPtr<mozIDOMWindowProxy> domwin;
143 0 : if (JSID_IS_INT(id)) {
144 0 : col->Item(JSID_TO_INT(id), getter_AddRefs(domwin));
145 0 : } else if (JSID_IS_STRING(id)) {
146 0 : nsAutoJSString idAsString;
147 0 : if (!idAsString.init(cx, JSID_TO_STRING(id))) {
148 0 : return false;
149 : }
150 0 : col->NamedItem(idAsString, getter_AddRefs(domwin));
151 : }
152 :
153 0 : return domwin != nullptr;
154 : }
155 :
156 : CrossOriginObjectType
157 1 : IdentifyCrossOriginObject(JSObject* obj)
158 : {
159 1 : obj = js::UncheckedUnwrap(obj, /* stopAtWindowProxy = */ false);
160 1 : const js::Class* clasp = js::GetObjectClass(obj);
161 1 : MOZ_ASSERT(!XrayUtils::IsXPCWNHolderClass(Jsvalify(clasp)), "shouldn't have a holder here");
162 :
163 1 : if (clasp->name[0] == 'L' && !strcmp(clasp->name, "Location"))
164 0 : return CrossOriginLocation;
165 1 : if (clasp->name[0] == 'W' && !strcmp(clasp->name, "Window"))
166 1 : return CrossOriginWindow;
167 :
168 0 : return CrossOriginOpaque;
169 : }
170 :
171 : bool
172 0 : AccessCheck::isCrossOriginAccessPermitted(JSContext* cx, HandleObject wrapper, HandleId id,
173 : Wrapper::Action act)
174 : {
175 0 : if (act == Wrapper::CALL)
176 0 : return false;
177 :
178 0 : if (act == Wrapper::ENUMERATE)
179 0 : return true;
180 :
181 : // For the case of getting a property descriptor, we allow if either GET or SET
182 : // is allowed, and rely on FilteringWrapper to filter out any disallowed accessors.
183 0 : if (act == Wrapper::GET_PROPERTY_DESCRIPTOR) {
184 0 : return isCrossOriginAccessPermitted(cx, wrapper, id, Wrapper::GET) ||
185 0 : isCrossOriginAccessPermitted(cx, wrapper, id, Wrapper::SET);
186 : }
187 :
188 0 : RootedObject obj(cx, js::UncheckedUnwrap(wrapper, /* stopAtWindowProxy = */ false));
189 0 : CrossOriginObjectType type = IdentifyCrossOriginObject(obj);
190 0 : if (JSID_IS_STRING(id)) {
191 0 : if (IsPermitted(type, JSID_TO_FLAT_STRING(id), act == Wrapper::SET))
192 0 : return true;
193 0 : } else if (type != CrossOriginOpaque &&
194 0 : IsCrossOriginWhitelistedSymbol(cx, id)) {
195 : // We always allow access to @@toStringTag, @@hasInstance, and
196 : // @@isConcatSpreadable. But then we nerf them to be a value descriptor
197 : // with value undefined in CrossOriginXrayWrapper.
198 0 : return true;
199 : }
200 :
201 0 : if (act != Wrapper::GET)
202 0 : return false;
203 :
204 : // Check for frame IDs. If we're resolving named frames, make sure to only
205 : // resolve ones that don't shadow native properties. See bug 860494.
206 0 : if (type == CrossOriginWindow) {
207 0 : if (JSID_IS_STRING(id)) {
208 0 : bool wouldShadow = false;
209 0 : if (!XrayUtils::HasNativeProperty(cx, wrapper, id, &wouldShadow) ||
210 : wouldShadow)
211 : {
212 : // If the named subframe matches the name of a DOM constructor,
213 : // the global resolve triggered by the HasNativeProperty call
214 : // above will try to perform a CheckedUnwrap on |wrapper|, and
215 : // throw a security error if it fails. That exception isn't
216 : // really useful for our callers, so we silence it and just
217 : // deny access to the property (since it matched a builtin).
218 : //
219 : // Note that this would be a problem if the resolve code ever
220 : // tried to CheckedUnwrap the wrapper _before_ concluding that
221 : // the name corresponds to a builtin global property, since it
222 : // would mean that we'd never permit cross-origin named subframe
223 : // access (something we regrettably need to support).
224 0 : JS_ClearPendingException(cx);
225 0 : return false;
226 : }
227 : }
228 0 : return IsFrameId(cx, obj, id);
229 : }
230 0 : return false;
231 : }
232 :
233 : bool
234 0 : AccessCheck::checkPassToPrivilegedCode(JSContext* cx, HandleObject wrapper, HandleValue v)
235 : {
236 : // Primitives are fine.
237 0 : if (!v.isObject())
238 0 : return true;
239 0 : RootedObject obj(cx, &v.toObject());
240 :
241 : // Non-wrappers are fine.
242 0 : if (!js::IsWrapper(obj))
243 0 : return true;
244 :
245 : // CPOWs use COWs (in the unprivileged junk scope) for all child->parent
246 : // references. Without this test, the child process wouldn't be able to
247 : // pass any objects at all to CPOWs.
248 0 : if (mozilla::jsipc::IsWrappedCPOW(obj) &&
249 0 : js::GetObjectCompartment(wrapper) == js::GetObjectCompartment(xpc::UnprivilegedJunkScope()) &&
250 0 : XRE_IsParentProcess())
251 : {
252 0 : return true;
253 : }
254 :
255 : // COWs are fine to pass to chrome if and only if they have __exposedProps__,
256 : // since presumably content should never have a reason to pass an opaque
257 : // object back to chrome.
258 0 : if (AccessCheck::isChrome(js::UncheckedUnwrap(wrapper)) && WrapperFactory::IsCOW(obj)) {
259 0 : RootedObject target(cx, js::UncheckedUnwrap(obj));
260 0 : JSAutoCompartment ac(cx, target);
261 0 : RootedId id(cx, GetJSIDByIndex(cx, XPCJSContext::IDX_EXPOSEDPROPS));
262 0 : bool found = false;
263 0 : if (!JS_HasPropertyById(cx, target, id, &found))
264 0 : return false;
265 0 : if (found)
266 0 : return true;
267 : }
268 :
269 : // Same-origin wrappers are fine.
270 0 : if (AccessCheck::wrapperSubsumes(obj))
271 0 : return true;
272 :
273 : // Badness.
274 0 : JS_ReportErrorASCII(cx, "Permission denied to pass object to privileged code");
275 0 : return false;
276 : }
277 :
278 : bool
279 0 : AccessCheck::checkPassToPrivilegedCode(JSContext* cx, HandleObject wrapper, const CallArgs& args)
280 : {
281 0 : if (!checkPassToPrivilegedCode(cx, wrapper, args.thisv()))
282 0 : return false;
283 0 : for (size_t i = 0; i < args.length(); ++i) {
284 0 : if (!checkPassToPrivilegedCode(cx, wrapper, args[i]))
285 0 : return false;
286 : }
287 0 : return true;
288 : }
289 :
290 : void
291 0 : AccessCheck::reportCrossOriginDenial(JSContext* cx, JS::HandleId id,
292 : const nsACString& accessType)
293 : {
294 : // This function exists because we want to report DOM SecurityErrors, not JS
295 : // Errors, when denying access on cross-origin DOM objects. It's
296 : // conceptually pretty similar to
297 : // AutoEnterPolicy::reportErrorIfExceptionIsNotPending.
298 0 : if (JS_IsExceptionPending(cx)) {
299 0 : return;
300 : }
301 :
302 0 : nsAutoCString message;
303 0 : if (JSID_IS_VOID(id)) {
304 0 : message = NS_LITERAL_CSTRING("Permission denied to access object");
305 : } else {
306 : // We want to use JS_ValueToSource here, because that most closely
307 : // matches what AutoEnterPolicy::reportErrorIfExceptionIsNotPending
308 : // does.
309 0 : JS::RootedValue idVal(cx, js::IdToValue(id));
310 0 : nsAutoJSString propName;
311 0 : JS::RootedString idStr(cx, JS_ValueToSource(cx, idVal));
312 0 : if (!idStr || !propName.init(cx, idStr)) {
313 0 : return;
314 : }
315 0 : message = NS_LITERAL_CSTRING("Permission denied to ") +
316 0 : accessType +
317 0 : NS_LITERAL_CSTRING(" property ") +
318 0 : NS_ConvertUTF16toUTF8(propName) +
319 0 : NS_LITERAL_CSTRING(" on cross-origin object");
320 : }
321 0 : ErrorResult rv;
322 0 : rv.ThrowDOMException(NS_ERROR_DOM_SECURITY_ERR, message);
323 0 : MOZ_ALWAYS_TRUE(rv.MaybeSetPendingException(cx));
324 : }
325 :
326 : enum Access { READ = (1<<0), WRITE = (1<<1), NO_ACCESS = 0 };
327 :
328 : static void
329 0 : EnterAndThrowASCII(JSContext* cx, JSObject* wrapper, const char* msg)
330 : {
331 0 : JSAutoCompartment ac(cx, wrapper);
332 0 : JS_ReportErrorASCII(cx, "%s", msg);
333 0 : }
334 :
335 : bool
336 0 : ExposedPropertiesOnly::check(JSContext* cx, HandleObject wrapper, HandleId id, Wrapper::Action act)
337 : {
338 0 : RootedObject wrappedObject(cx, Wrapper::wrappedObject(wrapper));
339 :
340 0 : if (act == Wrapper::CALL)
341 0 : return false;
342 :
343 : // For the case of getting a property descriptor, we allow if either GET or SET
344 : // is allowed, and rely on FilteringWrapper to filter out any disallowed accessors.
345 0 : if (act == Wrapper::GET_PROPERTY_DESCRIPTOR) {
346 0 : return check(cx, wrapper, id, Wrapper::GET) ||
347 0 : check(cx, wrapper, id, Wrapper::SET);
348 : }
349 :
350 0 : RootedId exposedPropsId(cx, GetJSIDByIndex(cx, XPCJSContext::IDX_EXPOSEDPROPS));
351 :
352 : // We need to enter the wrappee's compartment to look at __exposedProps__,
353 : // but we want to be in the wrapper's compartment if we call Deny().
354 : //
355 : // Unfortunately, |cx| can be in either compartment when we call ::check. :-(
356 0 : JSAutoCompartment ac(cx, wrappedObject);
357 :
358 0 : bool found = false;
359 0 : if (!JS_HasPropertyById(cx, wrappedObject, exposedPropsId, &found))
360 0 : return false;
361 :
362 : // If no __exposedProps__ existed, deny access.
363 0 : if (!found) {
364 : // Previously we automatically granted access to indexed properties and
365 : // .length for Array COWs. We're not doing that anymore, so make sure to
366 : // let people know what's going on.
367 : bool isArray;
368 0 : if (!JS_IsArrayObject(cx, wrappedObject, &isArray))
369 0 : return false;
370 0 : if (!isArray)
371 0 : isArray = JS_IsTypedArrayObject(wrappedObject);
372 0 : bool isIndexedAccessOnArray = isArray && JSID_IS_INT(id) && JSID_TO_INT(id) >= 0;
373 0 : bool isLengthAccessOnArray = isArray && JSID_IS_STRING(id) &&
374 0 : JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(id), "length");
375 0 : if (isIndexedAccessOnArray || isLengthAccessOnArray) {
376 0 : JSAutoCompartment ac2(cx, wrapper);
377 : ReportWrapperDenial(cx, id, WrapperDenialForCOW,
378 0 : "Access to elements and length of privileged Array not permitted");
379 : }
380 :
381 0 : return false;
382 : }
383 :
384 0 : if (id == JSID_VOID)
385 0 : return true;
386 :
387 0 : Rooted<PropertyDescriptor> desc(cx);
388 0 : if (!JS_GetPropertyDescriptorById(cx, wrappedObject, exposedPropsId, &desc))
389 0 : return false;
390 :
391 0 : if (!desc.object())
392 0 : return false;
393 :
394 0 : if (desc.hasGetterOrSetter()) {
395 0 : EnterAndThrowASCII(cx, wrapper, "__exposedProps__ must be a value property");
396 0 : return false;
397 : }
398 :
399 0 : RootedValue exposedProps(cx, desc.value());
400 0 : if (exposedProps.isNullOrUndefined())
401 0 : return false;
402 :
403 0 : if (!exposedProps.isObject()) {
404 0 : EnterAndThrowASCII(cx, wrapper, "__exposedProps__ must be undefined, null, or an Object");
405 0 : return false;
406 : }
407 :
408 0 : RootedObject hallpass(cx, &exposedProps.toObject());
409 :
410 0 : if (!AccessCheck::subsumes(js::UncheckedUnwrap(hallpass), wrappedObject)) {
411 0 : EnterAndThrowASCII(cx, wrapper, "Invalid __exposedProps__");
412 0 : return false;
413 : }
414 :
415 0 : Access access = NO_ACCESS;
416 :
417 0 : if (!JS_GetPropertyDescriptorById(cx, hallpass, id, &desc)) {
418 0 : return false; // Error
419 : }
420 0 : if (!desc.object() || !desc.enumerable())
421 0 : return false;
422 :
423 0 : if (!desc.value().isString()) {
424 0 : EnterAndThrowASCII(cx, wrapper, "property must be a string");
425 0 : return false;
426 : }
427 :
428 0 : JSFlatString* flat = JS_FlattenString(cx, desc.value().toString());
429 0 : if (!flat)
430 0 : return false;
431 :
432 0 : size_t length = JS_GetStringLength(JS_FORGET_STRING_FLATNESS(flat));
433 :
434 0 : for (size_t i = 0; i < length; ++i) {
435 0 : char16_t ch = JS_GetFlatStringCharAt(flat, i);
436 0 : switch (ch) {
437 : case 'r':
438 0 : if (access & READ) {
439 0 : EnterAndThrowASCII(cx, wrapper, "duplicate 'readable' property flag");
440 0 : return false;
441 : }
442 0 : access = Access(access | READ);
443 0 : break;
444 :
445 : case 'w':
446 0 : if (access & WRITE) {
447 0 : EnterAndThrowASCII(cx, wrapper, "duplicate 'writable' property flag");
448 0 : return false;
449 : }
450 0 : access = Access(access | WRITE);
451 0 : break;
452 :
453 : default:
454 0 : EnterAndThrowASCII(cx, wrapper, "properties can only be readable or read and writable");
455 0 : return false;
456 : }
457 : }
458 :
459 0 : if (access == NO_ACCESS) {
460 0 : EnterAndThrowASCII(cx, wrapper, "specified properties must have a permission bit set");
461 0 : return false;
462 : }
463 :
464 0 : if ((act == Wrapper::SET && !(access & WRITE)) ||
465 0 : (act != Wrapper::SET && !(access & READ))) {
466 0 : return false;
467 : }
468 :
469 : // Inspect the property on the underlying object to check for red flags.
470 0 : if (!JS_GetPropertyDescriptorById(cx, wrappedObject, id, &desc))
471 0 : return false;
472 :
473 : // Reject accessor properties.
474 0 : if (desc.hasGetterOrSetter()) {
475 0 : EnterAndThrowASCII(cx, wrapper, "Exposing privileged accessor properties is prohibited");
476 0 : return false;
477 : }
478 :
479 : // Reject privileged or cross-origin callables.
480 0 : if (desc.value().isObject()) {
481 0 : RootedObject maybeCallable(cx, js::UncheckedUnwrap(&desc.value().toObject()));
482 0 : if (JS::IsCallable(maybeCallable) && !AccessCheck::subsumes(wrapper, maybeCallable)) {
483 0 : EnterAndThrowASCII(cx, wrapper, "Exposing privileged or cross-origin callable is prohibited");
484 0 : return false;
485 : }
486 : }
487 :
488 0 : return true;
489 : }
490 :
491 : bool
492 0 : ExposedPropertiesOnly::deny(JSContext* cx, js::Wrapper::Action act, HandleId id,
493 : bool mayThrow)
494 : {
495 : // Fail silently for GET, ENUMERATE, and GET_PROPERTY_DESCRIPTOR.
496 0 : if (act == js::Wrapper::GET || act == js::Wrapper::ENUMERATE ||
497 : act == js::Wrapper::GET_PROPERTY_DESCRIPTOR)
498 : {
499 : // Note that ReportWrapperDenial doesn't do any _exception_ reporting,
500 : // so we want to do this regardless of the value of mayThrow.
501 : return ReportWrapperDenial(cx, id, WrapperDenialForCOW,
502 0 : "Access to privileged JS object not permitted");
503 : }
504 :
505 0 : return false;
506 : }
507 :
508 : } // namespace xpc
|