1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 : * vim: set ts=4 sw=4 et tw=99 ft=cpp:
3 : *
4 : * ***** BEGIN LICENSE BLOCK *****
5 : * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 : *
7 : * The contents of this file are subject to the Mozilla Public License Version
8 : * 1.1 (the "License"); you may not use this file except in compliance with
9 : * the License. You may obtain a copy of the License at
10 : * http://www.mozilla.org/MPL/
11 : *
12 : * Software distributed under the License is distributed on an "AS IS" basis,
13 : * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
14 : * for the specific language governing rights and limitations under the
15 : * License.
16 : *
17 : * The Original Code is mozilla.org code, released
18 : * June 24, 2010.
19 : *
20 : * The Initial Developer of the Original Code is
21 : * The Mozilla Foundation
22 : *
23 : * Contributor(s):
24 : * Andreas Gal <gal@mozilla.com>
25 : *
26 : * Alternatively, the contents of this file may be used under the terms of
27 : * either of the GNU General Public License Version 2 or later (the "GPL"),
28 : * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 : * in which case the provisions of the GPL or the LGPL are applicable instead
30 : * of those above. If you wish to allow use of your version of this file only
31 : * under the terms of either the GPL or the LGPL, and not to allow others to
32 : * use your version of this file under the terms of the MPL, indicate your
33 : * decision by deleting the provisions above and replace them with the notice
34 : * and other provisions required by the GPL or the LGPL. If you do not delete
35 : * the provisions above, a recipient may use your version of this file under
36 : * the terms of any one of the MPL, the GPL or the LGPL.
37 : *
38 : * ***** END LICENSE BLOCK ***** */
39 :
40 : #include "mozilla/Util.h"
41 :
42 : #include "AccessCheck.h"
43 :
44 : #include "nsJSPrincipals.h"
45 : #include "nsIDOMWindow.h"
46 : #include "nsIDOMWindowCollection.h"
47 : #include "nsContentUtils.h"
48 :
49 : #include "XPCWrapper.h"
50 : #include "XrayWrapper.h"
51 : #include "FilteringWrapper.h"
52 : #include "WrapperFactory.h"
53 :
54 : #include "jsfriendapi.h"
55 :
56 : using namespace mozilla;
57 : using namespace js;
58 :
59 : namespace xpc {
60 :
61 : nsIPrincipal *
62 1579142 : GetCompartmentPrincipal(JSCompartment *compartment)
63 : {
64 1579142 : return nsJSPrincipals::get(JS_GetCompartmentPrincipals(compartment));
65 : }
66 :
67 : bool
68 0 : AccessCheck::isSameOrigin(JSCompartment *a, JSCompartment *b)
69 : {
70 0 : nsIPrincipal *aprin = GetCompartmentPrincipal(a);
71 0 : nsIPrincipal *bprin = GetCompartmentPrincipal(b);
72 :
73 : // If either a or b doesn't have principals, we don't have enough
74 : // information to tell. Seeing as how this is Gecko, we are default-unsafe
75 : // in this case.
76 0 : if (!aprin || !bprin)
77 0 : return true;
78 :
79 : bool equals;
80 0 : nsresult rv = aprin->EqualsIgnoringDomain(bprin, &equals);
81 0 : if (NS_FAILED(rv)) {
82 0 : NS_ERROR("unable to ask about equality");
83 0 : return false;
84 : }
85 :
86 0 : return equals;
87 : }
88 :
89 : bool
90 0 : AccessCheck::isLocationObjectSameOrigin(JSContext *cx, JSObject *wrapper)
91 : {
92 : // Location objects are parented to the outer window for which they
93 : // were created. This gives us an easy way to determine whether our
94 : // object is same origin with the current inner window:
95 :
96 : // Grab the outer window...
97 0 : JSObject *obj = js::GetObjectParent(js::UnwrapObject(wrapper));
98 0 : if (!js::GetObjectClass(obj)->ext.innerObject) {
99 : // ...which might be wrapped in a security wrapper.
100 0 : obj = js::UnwrapObject(obj);
101 0 : JS_ASSERT(js::GetObjectClass(obj)->ext.innerObject);
102 : }
103 :
104 : // Now innerize it to find the *current* inner window for our outer.
105 0 : obj = JS_ObjectToInnerObject(cx, obj);
106 :
107 : // Which lets us compare the current compartment against the old one.
108 : return obj &&
109 : (isSameOrigin(js::GetObjectCompartment(wrapper),
110 0 : js::GetObjectCompartment(obj)) ||
111 0 : documentDomainMakesSameOrigin(cx, obj));
112 : }
113 :
114 : bool
115 353233 : AccessCheck::isChrome(JSCompartment *compartment)
116 : {
117 353233 : nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager();
118 353233 : if (!ssm) {
119 0 : return false;
120 : }
121 :
122 : bool privileged;
123 353233 : nsIPrincipal *principal = GetCompartmentPrincipal(compartment);
124 353233 : return NS_SUCCEEDED(ssm->IsSystemPrincipal(principal, &privileged)) && privileged;
125 : }
126 :
127 : nsIPrincipal *
128 1225564 : AccessCheck::getPrincipal(JSCompartment *compartment)
129 : {
130 1225564 : return GetCompartmentPrincipal(compartment);
131 : }
132 :
133 : #define NAME(ch, str, cases) \
134 : case ch: if (!strcmp(name, str)) switch (propChars[0]) { cases }; break;
135 : #define PROP(ch, actions) case ch: { actions }; break;
136 : #define RW(str) if (JS_FlatStringEqualsAscii(prop, str)) return true;
137 : #define R(str) if (!set && JS_FlatStringEqualsAscii(prop, str)) return true;
138 : #define W(str) if (set && JS_FlatStringEqualsAscii(prop, str)) return true;
139 :
140 : // Hardcoded policy for cross origin property access. This was culled from the
141 : // preferences file (all.js). We don't want users to overwrite highly sensitive
142 : // security policies.
143 : static bool
144 0 : IsPermitted(const char *name, JSFlatString *prop, bool set)
145 : {
146 : size_t propLength;
147 : const jschar *propChars =
148 0 : JS_GetInternedStringCharsAndLength(JS_FORGET_STRING_FLATNESS(prop), &propLength);
149 0 : if (!propLength)
150 0 : return false;
151 0 : switch (name[0]) {
152 0 : NAME('D', "DOMException",
153 : PROP('c', RW("code"))
154 : PROP('m', RW("message"))
155 : PROP('n', RW("name"))
156 : PROP('r', RW("result"))
157 : PROP('t', R("toString")))
158 0 : NAME('E', "Error",
159 : PROP('m', R("message")))
160 0 : NAME('H', "History",
161 : PROP('b', R("back"))
162 : PROP('f', R("forward"))
163 : PROP('g', R("go")))
164 0 : NAME('L', "Location",
165 : PROP('h', W("hash") W("href"))
166 : PROP('r', R("replace")))
167 0 : NAME('W', "Window",
168 : PROP('b', R("blur"))
169 : PROP('c', R("close") R("closed"))
170 : PROP('f', R("focus") R("frames"))
171 : PROP('h', R("history"))
172 : PROP('l', RW("location") R("length"))
173 : PROP('o', R("opener"))
174 : PROP('p', R("parent") R("postMessage"))
175 : PROP('s', R("self"))
176 : PROP('t', R("top"))
177 : PROP('w', R("window")))
178 : }
179 0 : return false;
180 : }
181 :
182 : #undef NAME
183 : #undef RW
184 : #undef R
185 : #undef W
186 :
187 : static bool
188 0 : IsFrameId(JSContext *cx, JSObject *obj, jsid id)
189 : {
190 0 : XPCWrappedNative *wn = XPCWrappedNative::GetWrappedNativeOfJSObject(cx, obj);
191 0 : if (!wn) {
192 0 : return false;
193 : }
194 :
195 0 : nsCOMPtr<nsIDOMWindow> domwin(do_QueryWrappedNative(wn));
196 0 : if (!domwin) {
197 0 : return false;
198 : }
199 :
200 0 : nsCOMPtr<nsIDOMWindowCollection> col;
201 0 : domwin->GetFrames(getter_AddRefs(col));
202 0 : if (!col) {
203 0 : return false;
204 : }
205 :
206 0 : if (JSID_IS_INT(id)) {
207 0 : col->Item(JSID_TO_INT(id), getter_AddRefs(domwin));
208 0 : } else if (JSID_IS_STRING(id)) {
209 0 : nsAutoString str(JS_GetInternedStringChars(JSID_TO_STRING(id)));
210 0 : col->NamedItem(str, getter_AddRefs(domwin));
211 : } else {
212 0 : return false;
213 : }
214 :
215 0 : return domwin != nsnull;
216 : }
217 :
218 : static bool
219 0 : IsWindow(const char *name)
220 : {
221 0 : return name[0] == 'W' && !strcmp(name, "Window");
222 : }
223 :
224 : static bool
225 0 : IsLocation(const char *name)
226 : {
227 0 : return name[0] == 'L' && !strcmp(name, "Location");
228 : }
229 :
230 : static nsIPrincipal *
231 0 : GetPrincipal(JSObject *obj)
232 : {
233 0 : NS_ASSERTION(!IS_SLIM_WRAPPER(obj), "global object is a slim wrapper?");
234 0 : if (!IS_WN_WRAPPER(obj)) {
235 0 : NS_ASSERTION(!(~js::GetObjectClass(obj)->flags &
236 : (JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE)),
237 : "bad object");
238 : nsCOMPtr<nsIScriptObjectPrincipal> objPrin =
239 0 : do_QueryInterface((nsISupports*)xpc_GetJSPrivate(obj));
240 0 : NS_ASSERTION(objPrin, "global isn't nsIScriptObjectPrincipal?");
241 0 : return objPrin->GetPrincipal();
242 : }
243 :
244 0 : nsIXPConnect *xpc = nsXPConnect::GetRuntimeInstance()->GetXPConnect();
245 0 : return xpc->GetPrincipal(obj, true);
246 : }
247 :
248 : bool
249 0 : AccessCheck::documentDomainMakesSameOrigin(JSContext *cx, JSObject *obj)
250 : {
251 0 : JSObject *scope = nsnull;
252 0 : JSStackFrame *fp = nsnull;
253 0 : JS_FrameIterator(cx, &fp);
254 0 : if (fp) {
255 0 : while (!JS_IsScriptFrame(cx, fp)) {
256 0 : if (!JS_FrameIterator(cx, &fp))
257 0 : break;
258 : }
259 :
260 0 : if (fp)
261 0 : scope = JS_GetGlobalForFrame(fp);
262 : }
263 :
264 0 : if (!scope)
265 0 : scope = JS_GetGlobalForScopeChain(cx);
266 :
267 : nsIPrincipal *subject;
268 : nsIPrincipal *object;
269 :
270 : {
271 0 : JSAutoEnterCompartment ac;
272 :
273 0 : if (!ac.enter(cx, scope))
274 0 : return false;
275 :
276 0 : subject = GetPrincipal(scope);
277 : }
278 :
279 0 : if (!subject)
280 0 : return false;
281 :
282 : {
283 0 : JSAutoEnterCompartment ac;
284 :
285 0 : if (!ac.enter(cx, obj))
286 0 : return false;
287 :
288 0 : object = GetPrincipal(JS_GetGlobalForObject(cx, obj));
289 : }
290 :
291 : bool subsumes;
292 0 : return NS_SUCCEEDED(subject->Subsumes(object, &subsumes)) && subsumes;
293 : }
294 :
295 : bool
296 0 : AccessCheck::isCrossOriginAccessPermitted(JSContext *cx, JSObject *wrapper, jsid id,
297 : Wrapper::Action act)
298 : {
299 0 : if (!XPCWrapper::GetSecurityManager())
300 0 : return true;
301 :
302 0 : if (act == Wrapper::CALL)
303 0 : return true;
304 :
305 0 : JSObject *obj = Wrapper::wrappedObject(wrapper);
306 :
307 : const char *name;
308 0 : js::Class *clasp = js::GetObjectClass(obj);
309 0 : NS_ASSERTION(Jsvalify(clasp) != &XrayUtils::HolderClass, "shouldn't have a holder here");
310 0 : if (clasp->ext.innerObject)
311 0 : name = "Window";
312 : else
313 0 : name = clasp->name;
314 :
315 0 : if (JSID_IS_STRING(id)) {
316 0 : if (IsPermitted(name, JSID_TO_FLAT_STRING(id), act == Wrapper::SET))
317 0 : return true;
318 : }
319 :
320 0 : if (IsWindow(name) && IsFrameId(cx, obj, id))
321 0 : return true;
322 :
323 : // We only reach this point for cross origin location objects (see
324 : // SameOriginOrCrossOriginAccessiblePropertiesOnly::check).
325 0 : if (!IsLocation(name) && documentDomainMakesSameOrigin(cx, obj))
326 0 : return true;
327 :
328 : return (act == Wrapper::SET)
329 : ? nsContentUtils::IsCallerTrustedForWrite()
330 0 : : nsContentUtils::IsCallerTrustedForRead();
331 : }
332 :
333 : bool
334 0 : AccessCheck::isSystemOnlyAccessPermitted(JSContext *cx)
335 : {
336 0 : nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager();
337 0 : if (!ssm) {
338 0 : return true;
339 : }
340 :
341 : JSStackFrame *fp;
342 0 : nsIPrincipal *principal = ssm->GetCxSubjectPrincipalAndFrame(cx, &fp);
343 0 : if (!principal) {
344 0 : return false;
345 : }
346 :
347 0 : if (!fp) {
348 0 : if (!JS_FrameIterator(cx, &fp)) {
349 : // No code at all is running. So we must be arriving here as the result
350 : // of C++ code asking us to do something. Allow access.
351 0 : return true;
352 : }
353 :
354 : // Some code is running, we can't make the assumption, as above, but we
355 : // can't use a native frame, so clear fp.
356 0 : fp = NULL;
357 0 : } else if (!JS_IsScriptFrame(cx, fp)) {
358 0 : fp = NULL;
359 : }
360 :
361 : bool privileged;
362 0 : if (NS_SUCCEEDED(ssm->IsSystemPrincipal(principal, &privileged)) &&
363 : privileged) {
364 0 : return true;
365 : }
366 :
367 : // Allow any code loaded from chrome://global/ to touch us, even if it was
368 : // cloned into a less privileged context.
369 : static const char prefix[] = "chrome://global/";
370 : const char *filename;
371 0 : if (fp &&
372 0 : (filename = JS_GetScriptFilename(cx, JS_GetFrameScript(cx, fp))) &&
373 0 : !strncmp(filename, prefix, ArrayLength(prefix) - 1)) {
374 0 : return true;
375 : }
376 :
377 0 : return NS_SUCCEEDED(ssm->IsCapabilityEnabled("UniversalXPConnect", &privileged)) && privileged;
378 : }
379 :
380 : bool
381 0 : AccessCheck::needsSystemOnlyWrapper(JSObject *obj)
382 : {
383 0 : if (!IS_WN_WRAPPER(obj))
384 0 : return false;
385 :
386 0 : XPCWrappedNative *wn = static_cast<XPCWrappedNative *>(js::GetObjectPrivate(obj));
387 0 : return wn->NeedsSOW();
388 : }
389 :
390 : bool
391 46 : AccessCheck::isScriptAccessOnly(JSContext *cx, JSObject *wrapper)
392 : {
393 46 : JS_ASSERT(js::IsWrapper(wrapper));
394 :
395 : unsigned flags;
396 46 : JSObject *obj = js::UnwrapObject(wrapper, true, &flags);
397 :
398 : // If the wrapper indicates script-only access, we are done.
399 46 : if (flags & WrapperFactory::SCRIPT_ACCESS_ONLY_FLAG) {
400 0 : if (flags & WrapperFactory::SOW_FLAG)
401 0 : return !isSystemOnlyAccessPermitted(cx);
402 :
403 0 : if (flags & WrapperFactory::PARTIALLY_TRANSPARENT)
404 0 : return !XrayUtils::IsTransparent(cx, wrapper);
405 :
406 0 : nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager();
407 0 : if (!ssm)
408 0 : return true;
409 :
410 : // Bypass script-only status if UniversalXPConnect is enabled.
411 : bool privileged;
412 0 : return !NS_SUCCEEDED(ssm->IsCapabilityEnabled("UniversalXPConnect", &privileged)) ||
413 0 : !privileged;
414 : }
415 :
416 : // In addition, chrome objects can explicitly opt-in by setting .scriptOnly to true.
417 46 : if (js::GetProxyHandler(wrapper) ==
418 : &FilteringWrapper<CrossCompartmentSecurityWrapper,
419 : CrossOriginAccessiblePropertiesOnly>::singleton) {
420 0 : jsid scriptOnlyId = GetRTIdByIndex(cx, XPCJSRuntime::IDX_SCRIPTONLY);
421 : jsval scriptOnly;
422 0 : if (JS_LookupPropertyById(cx, obj, scriptOnlyId, &scriptOnly) &&
423 0 : scriptOnly == JSVAL_TRUE)
424 0 : return true; // script-only
425 : }
426 :
427 : // Allow non-script access to same-origin location objects and any other
428 : // objects.
429 46 : return WrapperFactory::IsLocationObject(obj) && !isLocationObjectSameOrigin(cx, wrapper);
430 : }
431 :
432 : void
433 0 : AccessCheck::deny(JSContext *cx, jsid id)
434 : {
435 0 : if (id == JSID_VOID) {
436 0 : JS_ReportError(cx, "Permission denied to access object");
437 : } else {
438 : jsval idval;
439 0 : if (!JS_IdToValue(cx, id, &idval))
440 0 : return;
441 0 : JSString *str = JS_ValueToString(cx, idval);
442 0 : if (!str)
443 0 : return;
444 0 : const jschar *chars = JS_GetStringCharsZ(cx, str);
445 0 : if (chars)
446 0 : JS_ReportError(cx, "Permission denied to access property '%hs'", chars);
447 : }
448 : }
449 :
450 : enum Access { READ = (1<<0), WRITE = (1<<1), NO_ACCESS = 0 };
451 :
452 : static bool
453 0 : Deny(JSContext *cx, jsid id, Wrapper::Action act)
454 : {
455 : // Refuse to perform the action and just return the default value.
456 0 : if (act == Wrapper::GET)
457 0 : return true;
458 : // If its a set, deny it and throw an exception.
459 0 : AccessCheck::deny(cx, id);
460 0 : return false;
461 : }
462 :
463 : bool
464 0 : PermitIfUniversalXPConnect(JSContext *cx, jsid id, Wrapper::Action act,
465 : ExposedPropertiesOnly::Permission &perm)
466 : {
467 : // If UniversalXPConnect is enabled, allow access even if __exposedProps__ doesn't
468 : // exists.
469 0 : nsIScriptSecurityManager *ssm = XPCWrapper::GetSecurityManager();
470 0 : if (!ssm) {
471 0 : return false;
472 : }
473 : bool privileged;
474 0 : if (NS_SUCCEEDED(ssm->IsCapabilityEnabled("UniversalXPConnect", &privileged)) &&
475 : privileged) {
476 0 : perm = ExposedPropertiesOnly::PermitPropertyAccess;
477 0 : return true; // Allow
478 : }
479 :
480 : // Deny
481 0 : return Deny(cx, id, act);
482 : }
483 :
484 : bool
485 0 : ExposedPropertiesOnly::check(JSContext *cx, JSObject *wrapper, jsid id, Wrapper::Action act,
486 : Permission &perm)
487 : {
488 0 : JSObject *wrappedObject = Wrapper::wrappedObject(wrapper);
489 :
490 0 : if (act == Wrapper::CALL) {
491 0 : perm = PermitObjectAccess;
492 0 : return true;
493 : }
494 :
495 0 : perm = DenyAccess;
496 :
497 0 : jsid exposedPropsId = GetRTIdByIndex(cx, XPCJSRuntime::IDX_EXPOSEDPROPS);
498 :
499 0 : JSBool found = false;
500 0 : JSAutoEnterCompartment ac;
501 0 : if (!ac.enter(cx, wrappedObject) ||
502 0 : !JS_HasPropertyById(cx, wrappedObject, exposedPropsId, &found))
503 0 : return false;
504 :
505 : // Always permit access to "length" and indexed properties of arrays.
506 0 : if (JS_IsArrayObject(cx, wrappedObject) &&
507 0 : ((JSID_IS_INT(id) && JSID_TO_INT(id) >= 0) ||
508 0 : (JSID_IS_STRING(id) && JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(id), "length")))) {
509 0 : perm = PermitPropertyAccess;
510 0 : return true; // Allow
511 : }
512 :
513 : // If no __exposedProps__ existed, deny access.
514 0 : if (!found) {
515 : // For now, only do this on functions.
516 0 : if (!JS_ObjectIsFunction(cx, wrappedObject)) {
517 0 : perm = PermitPropertyAccess;
518 0 : return true;
519 : }
520 0 : return PermitIfUniversalXPConnect(cx, id, act, perm); // Deny
521 : }
522 :
523 0 : if (id == JSID_VOID) {
524 : // This will force the caller to call us back for individual property accesses.
525 0 : perm = PermitPropertyAccess;
526 0 : return true;
527 : }
528 :
529 : jsval exposedProps;
530 0 : if (!JS_LookupPropertyById(cx, wrappedObject, exposedPropsId, &exposedProps))
531 0 : return false;
532 :
533 0 : if (JSVAL_IS_VOID(exposedProps) || JSVAL_IS_NULL(exposedProps)) {
534 0 : return PermitIfUniversalXPConnect(cx, id, act, perm); // Deny
535 : }
536 :
537 0 : if (!JSVAL_IS_OBJECT(exposedProps)) {
538 0 : JS_ReportError(cx, "__exposedProps__ must be undefined, null, or an Object");
539 0 : return false;
540 : }
541 :
542 0 : JSObject *hallpass = JSVAL_TO_OBJECT(exposedProps);
543 :
544 0 : Access access = NO_ACCESS;
545 :
546 : JSPropertyDescriptor desc;
547 0 : if (!JS_GetPropertyDescriptorById(cx, hallpass, id, JSRESOLVE_QUALIFIED, &desc)) {
548 0 : return false; // Error
549 : }
550 0 : if (desc.obj == NULL || !(desc.attrs & JSPROP_ENUMERATE)) {
551 0 : return PermitIfUniversalXPConnect(cx, id, act, perm); // Deny
552 : }
553 :
554 0 : if (!JSVAL_IS_STRING(desc.value)) {
555 0 : JS_ReportError(cx, "property must be a string");
556 0 : return false;
557 : }
558 :
559 0 : JSString *str = JSVAL_TO_STRING(desc.value);
560 : size_t length;
561 0 : const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length);
562 0 : if (!chars)
563 0 : return false;
564 :
565 0 : for (size_t i = 0; i < length; ++i) {
566 0 : switch (chars[i]) {
567 : case 'r':
568 0 : if (access & READ) {
569 0 : JS_ReportError(cx, "duplicate 'readable' property flag");
570 0 : return false;
571 : }
572 0 : access = Access(access | READ);
573 0 : break;
574 :
575 : case 'w':
576 0 : if (access & WRITE) {
577 0 : JS_ReportError(cx, "duplicate 'writable' property flag");
578 0 : return false;
579 : }
580 0 : access = Access(access | WRITE);
581 0 : break;
582 :
583 : default:
584 0 : JS_ReportError(cx, "properties can only be readable or read and writable");
585 0 : return false;
586 : }
587 : }
588 :
589 0 : if (access == NO_ACCESS) {
590 0 : JS_ReportError(cx, "specified properties must have a permission bit set");
591 0 : return false;
592 : }
593 :
594 0 : if ((act == Wrapper::SET && !(access & WRITE)) ||
595 0 : (act != Wrapper::SET && !(access & READ))) {
596 0 : return PermitIfUniversalXPConnect(cx, id, act, perm); // Deny
597 : }
598 :
599 0 : perm = PermitPropertyAccess;
600 0 : return true; // Allow
601 : }
602 :
603 : }
|