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/. */
6 
7 #include "xpcprivate.h"
8 #include "WrapperFactory.h"
9 #include "AccessCheck.h"
10 #include "jsfriendapi.h"
11 #include "js/Exception.h"
12 #include "js/Proxy.h"
13 #include "js/Wrapper.h"
14 #include "mozilla/ErrorResult.h"
15 #include "mozilla/Unused.h"
16 #include "mozilla/dom/BindingUtils.h"
17 #include "mozilla/dom/BlobBinding.h"
18 #include "mozilla/dom/BlobImpl.h"
19 #include "mozilla/dom/File.h"
20 #include "mozilla/dom/StructuredCloneHolder.h"
21 #include "nsContentUtils.h"
22 #include "nsGlobalWindow.h"
23 #include "nsJSUtils.h"
24 #include "js/Object.h"  // JS::GetCompartment
25 
26 using namespace mozilla;
27 using namespace mozilla::dom;
28 using namespace JS;
29 
30 namespace xpc {
31 
IsReflector(JSObject * obj,JSContext * cx)32 bool IsReflector(JSObject* obj, JSContext* cx) {
33   obj = js::CheckedUnwrapDynamic(obj, cx, /* stopAtWindowProxy = */ false);
34   if (!obj) {
35     return false;
36   }
37   return IS_WN_REFLECTOR(obj) || dom::IsDOMObject(obj);
38 }
39 
40 enum StackScopedCloneTags {
41   SCTAG_BASE = JS_SCTAG_USER_MIN,
42   SCTAG_REFLECTOR,
43   SCTAG_BLOB,
44   SCTAG_FUNCTION,
45 };
46 
47 class MOZ_STACK_CLASS StackScopedCloneData : public StructuredCloneHolderBase {
48  public:
StackScopedCloneData(JSContext * aCx,StackScopedCloneOptions * aOptions)49   StackScopedCloneData(JSContext* aCx, StackScopedCloneOptions* aOptions)
50       : mOptions(aOptions), mReflectors(aCx), mFunctions(aCx) {}
51 
~StackScopedCloneData()52   ~StackScopedCloneData() { Clear(); }
53 
CustomReadHandler(JSContext * aCx,JSStructuredCloneReader * aReader,const JS::CloneDataPolicy & aCloneDataPolicy,uint32_t aTag,uint32_t aData)54   JSObject* CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader,
55                               const JS::CloneDataPolicy& aCloneDataPolicy,
56                               uint32_t aTag, uint32_t aData) override {
57     if (aTag == SCTAG_REFLECTOR) {
58       MOZ_ASSERT(!aData);
59 
60       size_t idx;
61       if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
62         return nullptr;
63       }
64 
65       RootedObject reflector(aCx, mReflectors[idx]);
66       MOZ_ASSERT(reflector, "No object pointer?");
67       MOZ_ASSERT(IsReflector(reflector, aCx),
68                  "Object pointer must be a reflector!");
69 
70       if (!JS_WrapObject(aCx, &reflector)) {
71         return nullptr;
72       }
73 
74       return reflector;
75     }
76 
77     if (aTag == SCTAG_FUNCTION) {
78       MOZ_ASSERT(aData < mFunctions.length());
79 
80       RootedValue functionValue(aCx);
81       RootedObject obj(aCx, mFunctions[aData]);
82 
83       if (!JS_WrapObject(aCx, &obj)) {
84         return nullptr;
85       }
86 
87       FunctionForwarderOptions forwarderOptions;
88       if (!xpc::NewFunctionForwarder(aCx, JSID_VOIDHANDLE, obj,
89                                      forwarderOptions, &functionValue)) {
90         return nullptr;
91       }
92 
93       return &functionValue.toObject();
94     }
95 
96     if (aTag == SCTAG_BLOB) {
97       MOZ_ASSERT(!aData);
98 
99       size_t idx;
100       if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
101         return nullptr;
102       }
103 
104       nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
105       MOZ_ASSERT(global);
106 
107       // RefPtr<File> needs to go out of scope before toObjectOrNull() is called
108       // because otherwise the static analysis thinks it can gc the JSObject via
109       // the stack.
110       JS::Rooted<JS::Value> val(aCx);
111       {
112         RefPtr<Blob> blob = Blob::Create(global, mBlobImpls[idx]);
113         if (NS_WARN_IF(!blob)) {
114           return nullptr;
115         }
116 
117         if (!ToJSValue(aCx, blob, &val)) {
118           return nullptr;
119         }
120       }
121 
122       return val.toObjectOrNull();
123     }
124 
125     MOZ_ASSERT_UNREACHABLE("Encountered garbage in the clone stream!");
126     return nullptr;
127   }
128 
CustomWriteHandler(JSContext * aCx,JSStructuredCloneWriter * aWriter,JS::Handle<JSObject * > aObj,bool * aSameProcessScopeRequired)129   bool CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter,
130                           JS::Handle<JSObject*> aObj,
131                           bool* aSameProcessScopeRequired) override {
132     {
133       JS::Rooted<JSObject*> obj(aCx, aObj);
134       Blob* blob = nullptr;
135       if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
136         BlobImpl* blobImpl = blob->Impl();
137         MOZ_ASSERT(blobImpl);
138 
139         // XXX(Bug 1631371) Check if this should use a fallible operation as it
140         // pretended earlier.
141         mBlobImpls.AppendElement(blobImpl);
142 
143         size_t idx = mBlobImpls.Length() - 1;
144         return JS_WriteUint32Pair(aWriter, SCTAG_BLOB, 0) &&
145                JS_WriteBytes(aWriter, &idx, sizeof(size_t));
146       }
147     }
148 
149     if (mOptions->wrapReflectors && IsReflector(aObj, aCx)) {
150       if (!mReflectors.append(aObj)) {
151         return false;
152       }
153 
154       size_t idx = mReflectors.length() - 1;
155       if (!JS_WriteUint32Pair(aWriter, SCTAG_REFLECTOR, 0)) {
156         return false;
157       }
158       if (!JS_WriteBytes(aWriter, &idx, sizeof(size_t))) {
159         return false;
160       }
161       return true;
162     }
163 
164     if (JS::IsCallable(aObj)) {
165       if (mOptions->cloneFunctions) {
166         if (!mFunctions.append(aObj)) {
167           return false;
168         }
169         return JS_WriteUint32Pair(aWriter, SCTAG_FUNCTION,
170                                   mFunctions.length() - 1);
171       } else {
172         JS_ReportErrorASCII(
173             aCx, "Permission denied to pass a Function via structured clone");
174         return false;
175       }
176     }
177 
178     JS_ReportErrorASCII(aCx,
179                         "Encountered unsupported value type writing "
180                         "stack-scoped structured clone");
181     return false;
182   }
183 
184   StackScopedCloneOptions* mOptions;
185   RootedObjectVector mReflectors;
186   RootedObjectVector mFunctions;
187   nsTArray<RefPtr<BlobImpl>> mBlobImpls;
188 };
189 
190 /*
191  * General-purpose structured-cloning utility for cases where the structured
192  * clone buffer is only used in stack-scope (that is to say, the buffer does
193  * not escape from this function). The stack-scoping allows us to pass
194  * references to various JSObjects directly in certain situations without
195  * worrying about lifetime issues.
196  *
197  * This function assumes that |cx| is already entered the compartment we want
198  * to clone to, and that |val| may not be same-compartment with cx. When the
199  * function returns, |val| is set to the result of the clone.
200  */
StackScopedClone(JSContext * cx,StackScopedCloneOptions & options,HandleObject sourceScope,MutableHandleValue val)201 bool StackScopedClone(JSContext* cx, StackScopedCloneOptions& options,
202                       HandleObject sourceScope, MutableHandleValue val) {
203   StackScopedCloneData data(cx, &options);
204   {
205     // For parsing val we have to enter (a realm in) its compartment.
206     JSAutoRealm ar(cx, sourceScope);
207     if (!data.Write(cx, val)) {
208       return false;
209     }
210   }
211 
212   // Now recreate the clones in the target realm.
213   if (!data.Read(cx, val)) {
214     return false;
215   }
216 
217   // Deep-freeze if requested.
218   if (options.deepFreeze && val.isObject()) {
219     RootedObject obj(cx, &val.toObject());
220     if (!JS_DeepFreezeObject(cx, obj)) {
221       return false;
222     }
223   }
224 
225   return true;
226 }
227 
228 // Note - This function mirrors the logic of CheckPassToChrome in
229 // ChromeObjectWrapper.cpp.
CheckSameOriginArg(JSContext * cx,FunctionForwarderOptions & options,HandleValue v)230 static bool CheckSameOriginArg(JSContext* cx, FunctionForwarderOptions& options,
231                                HandleValue v) {
232   // Consumers can explicitly opt out of this security check. This is used in
233   // the web console to allow the utility functions to accept cross-origin
234   // Windows.
235   if (options.allowCrossOriginArguments) {
236     return true;
237   }
238 
239   // Primitives are fine.
240   if (!v.isObject()) {
241     return true;
242   }
243   RootedObject obj(cx, &v.toObject());
244   MOZ_ASSERT(JS::GetCompartment(obj) != js::GetContextCompartment(cx),
245              "This should be invoked after entering the compartment but before "
246              "wrapping the values");
247 
248   // Non-wrappers are fine.
249   if (!js::IsWrapper(obj)) {
250     return true;
251   }
252 
253   // Wrappers leading back to the scope of the exported function are fine.
254   if (JS::GetCompartment(js::UncheckedUnwrap(obj)) ==
255       js::GetContextCompartment(cx)) {
256     return true;
257   }
258 
259   // Same-origin wrappers are fine.
260   if (AccessCheck::wrapperSubsumes(obj)) {
261     return true;
262   }
263 
264   // Badness.
265   JS_ReportErrorASCII(cx,
266                       "Permission denied to pass object to exported function");
267   return false;
268 }
269 
270 // Sanitize the exception on cx (which comes from calling unwrappedFun), if the
271 // current Realm of cx shouldn't have access to it.  unwrappedFun is generally
272 // _not_ in the current Realm of cx here.
MaybeSanitizeException(JSContext * cx,JS::Handle<JSObject * > unwrappedFun)273 static void MaybeSanitizeException(JSContext* cx,
274                                    JS::Handle<JSObject*> unwrappedFun) {
275   // Ensure that we are not propagating more-privileged exceptions
276   // to less-privileged code.
277   nsIPrincipal* callerPrincipal = nsContentUtils::SubjectPrincipal(cx);
278 
279   // Re-enter the unwrappedFun Realm to do get the current exception, so we
280   // don't end up unnecessarily wrapping exceptions.
281   {  // Scope for JSAutoRealm
282     JSAutoRealm ar(cx, unwrappedFun);
283 
284     JS::ExceptionStack exnStack(cx);
285     // If JS::GetPendingExceptionStack returns false, this was an uncatchable
286     // exception, or we somehow failed to wrap the exception into our
287     // compartment.  In either case, treating this as uncatchable exception,
288     // by returning without setting any exception on the JSContext,
289     // seems fine.
290     if (!JS::GetPendingExceptionStack(cx, &exnStack)) {
291       JS_ClearPendingException(cx);
292       return;
293     }
294 
295     // Let through non-objects as-is, because some APIs rely on
296     // that and accidental exceptions are never non-objects.
297     if (!exnStack.exception().isObject() ||
298         callerPrincipal->Subsumes(nsContentUtils::ObjectPrincipal(
299             js::UncheckedUnwrap(&exnStack.exception().toObject())))) {
300       // Just leave exn as-is.
301       return;
302     }
303 
304     // Whoever we are throwing the exception to should not have access to
305     // the exception.  Sanitize it. First clear the existing exception.
306     JS_ClearPendingException(cx);
307     {  // Scope for AutoJSAPI
308       AutoJSAPI jsapi;
309       if (jsapi.Init(unwrappedFun)) {
310         JS::SetPendingExceptionStack(cx, exnStack);
311       }
312       // If Init() fails, we can't report the exception, but oh, well.
313 
314       // Now just let the AutoJSAPI go out of scope and it will report the
315       // exception in its destructor.
316     }
317   }
318 
319   // Now back in our original Realm again, throw a sanitized exception.
320   ErrorResult rv;
321   rv.ThrowInvalidStateError("An exception was thrown");
322   // Can we provide a better context here?
323   Unused << rv.MaybeSetPendingException(cx);
324 }
325 
FunctionForwarder(JSContext * cx,unsigned argc,Value * vp)326 static bool FunctionForwarder(JSContext* cx, unsigned argc, Value* vp) {
327   CallArgs args = CallArgsFromVp(argc, vp);
328 
329   // Grab the options from the reserved slot.
330   RootedObject optionsObj(
331       cx, &js::GetFunctionNativeReserved(&args.callee(), 1).toObject());
332   FunctionForwarderOptions options(cx, optionsObj);
333   if (!options.Parse()) {
334     return false;
335   }
336 
337   // Grab and unwrap the underlying callable.
338   RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
339   RootedObject unwrappedFun(cx, js::UncheckedUnwrap(&v.toObject()));
340 
341   RootedValue thisVal(cx, NullValue());
342   if (!args.isConstructing()) {
343     RootedObject thisObject(cx);
344     if (!args.computeThis(cx, &thisObject)) {
345       return false;
346     }
347     thisVal.setObject(*thisObject);
348   }
349 
350   bool ok = true;
351   {
352     // We manually implement the contents of CrossCompartmentWrapper::call
353     // here, because certain function wrappers (notably content->nsEP) are
354     // not callable.
355     JSAutoRealm ar(cx, unwrappedFun);
356     bool crossCompartment =
357         JS::GetCompartment(unwrappedFun) != JS::GetCompartment(&args.callee());
358     if (crossCompartment) {
359       if (!CheckSameOriginArg(cx, options, thisVal) ||
360           !JS_WrapValue(cx, &thisVal)) {
361         return false;
362       }
363 
364       for (size_t n = 0; n < args.length(); ++n) {
365         if (!CheckSameOriginArg(cx, options, args[n]) ||
366             !JS_WrapValue(cx, args[n])) {
367           return false;
368         }
369       }
370     }
371 
372     RootedValue fval(cx, ObjectValue(*unwrappedFun));
373     if (args.isConstructing()) {
374       RootedObject obj(cx);
375       ok = JS::Construct(cx, fval, args, &obj);
376       if (ok) {
377         args.rval().setObject(*obj);
378       }
379     } else {
380       ok = JS::Call(cx, thisVal, fval, args, args.rval());
381     }
382   }
383 
384   // Now that we are back in our original Realm, we can check whether to
385   // sanitize the exception.
386   if (!ok) {
387     MaybeSanitizeException(cx, unwrappedFun);
388     return false;
389   }
390 
391   // Rewrap the return value into our compartment.
392   return JS_WrapValue(cx, args.rval());
393 }
394 
NewFunctionForwarder(JSContext * cx,HandleId idArg,HandleObject callable,FunctionForwarderOptions & options,MutableHandleValue vp)395 bool NewFunctionForwarder(JSContext* cx, HandleId idArg, HandleObject callable,
396                           FunctionForwarderOptions& options,
397                           MutableHandleValue vp) {
398   RootedId id(cx, idArg);
399   if (id == JSID_VOIDHANDLE) {
400     id = GetJSIDByIndex(cx, XPCJSContext::IDX_EMPTYSTRING);
401   }
402 
403   // If our callable is a (possibly wrapped) function, we can give
404   // the exported thing the right number of args.
405   unsigned nargs = 0;
406   RootedObject unwrapped(cx, js::UncheckedUnwrap(callable));
407   if (unwrapped) {
408     if (JSFunction* fun = JS_GetObjectFunction(unwrapped)) {
409       nargs = JS_GetFunctionArity(fun);
410     }
411   }
412 
413   // We have no way of knowing whether the underlying function wants to be a
414   // constructor or not, so we just mark all forwarders as constructors, and
415   // let the underlying function throw for construct calls if it wants.
416   JSFunction* fun = js::NewFunctionByIdWithReserved(
417       cx, FunctionForwarder, nargs, JSFUN_CONSTRUCTOR, id);
418   if (!fun) {
419     return false;
420   }
421 
422   // Stash the callable in slot 0.
423   AssertSameCompartment(cx, callable);
424   RootedObject funobj(cx, JS_GetFunctionObject(fun));
425   js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
426 
427   // Stash the options in slot 1.
428   RootedObject optionsObj(cx, options.ToJSObject(cx));
429   if (!optionsObj) {
430     return false;
431   }
432   js::SetFunctionNativeReserved(funobj, 1, ObjectValue(*optionsObj));
433 
434   vp.setObject(*funobj);
435   return true;
436 }
437 
ExportFunction(JSContext * cx,HandleValue vfunction,HandleValue vscope,HandleValue voptions,MutableHandleValue rval)438 bool ExportFunction(JSContext* cx, HandleValue vfunction, HandleValue vscope,
439                     HandleValue voptions, MutableHandleValue rval) {
440   bool hasOptions = !voptions.isUndefined();
441   if (!vscope.isObject() || !vfunction.isObject() ||
442       (hasOptions && !voptions.isObject())) {
443     JS_ReportErrorASCII(cx, "Invalid argument");
444     return false;
445   }
446 
447   RootedObject funObj(cx, &vfunction.toObject());
448   RootedObject targetScope(cx, &vscope.toObject());
449   ExportFunctionOptions options(cx,
450                                 hasOptions ? &voptions.toObject() : nullptr);
451   if (hasOptions && !options.Parse()) {
452     return false;
453   }
454 
455   // Restrictions:
456   // * We must subsume the scope we are exporting to.
457   // * We must subsume the function being exported, because the function
458   //   forwarder manually circumvents security wrapper CALL restrictions.
459   targetScope = js::CheckedUnwrapDynamic(targetScope, cx);
460   // For the function we can just CheckedUnwrapStatic, because if it's
461   // not callable we're going to fail out anyway.
462   funObj = js::CheckedUnwrapStatic(funObj);
463   if (!targetScope || !funObj) {
464     JS_ReportErrorASCII(cx, "Permission denied to export function into scope");
465     return false;
466   }
467 
468   if (js::IsScriptedProxy(targetScope)) {
469     JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
470     return false;
471   }
472 
473   {
474     // We need to operate in the target scope from here on, let's enter
475     // its realm.
476     JSAutoRealm ar(cx, targetScope);
477 
478     // Unwrapping to see if we have a callable.
479     funObj = UncheckedUnwrap(funObj);
480     if (!JS::IsCallable(funObj)) {
481       JS_ReportErrorASCII(cx, "First argument must be a function");
482       return false;
483     }
484 
485     RootedId id(cx, options.defineAs);
486     if (JSID_IS_VOID(id)) {
487       // If there wasn't any function name specified, copy the name from the
488       // function being imported.  But be careful in case the callable we have
489       // is not actually a JSFunction.
490       RootedString funName(cx);
491       JSFunction* fun = JS_GetObjectFunction(funObj);
492       if (fun) {
493         funName = JS_GetFunctionId(fun);
494       }
495       if (!funName) {
496         funName = JS_AtomizeAndPinString(cx, "");
497       }
498       JS_MarkCrossZoneIdValue(cx, StringValue(funName));
499 
500       if (!JS_StringToId(cx, funName, &id)) {
501         return false;
502       }
503     } else {
504       JS_MarkCrossZoneId(cx, id);
505     }
506     MOZ_ASSERT(JSID_IS_STRING(id));
507 
508     // The function forwarder will live in the target compartment. Since
509     // this function will be referenced from its private slot, to avoid a
510     // GC hazard, we must wrap it to the same compartment.
511     if (!JS_WrapObject(cx, &funObj)) {
512       return false;
513     }
514 
515     // And now, let's create the forwarder function in the target compartment
516     // for the function the be exported.
517     FunctionForwarderOptions forwarderOptions;
518     forwarderOptions.allowCrossOriginArguments =
519         options.allowCrossOriginArguments;
520     if (!NewFunctionForwarder(cx, id, funObj, forwarderOptions, rval)) {
521       JS_ReportErrorASCII(cx, "Exporting function failed");
522       return false;
523     }
524 
525     // We have the forwarder function in the target compartment. If
526     // defineAs was set, we also need to define it as a property on
527     // the target.
528     if (!JSID_IS_VOID(options.defineAs)) {
529       if (!JS_DefinePropertyById(cx, targetScope, id, rval, JSPROP_ENUMERATE)) {
530         return false;
531       }
532     }
533   }
534 
535   // Finally we have to re-wrap the exported function back to the caller
536   // compartment.
537   if (!JS_WrapValue(cx, rval)) {
538     return false;
539   }
540 
541   return true;
542 }
543 
CreateObjectIn(JSContext * cx,HandleValue vobj,CreateObjectInOptions & options,MutableHandleValue rval)544 bool CreateObjectIn(JSContext* cx, HandleValue vobj,
545                     CreateObjectInOptions& options, MutableHandleValue rval) {
546   if (!vobj.isObject()) {
547     JS_ReportErrorASCII(cx, "Expected an object as the target scope");
548     return false;
549   }
550 
551   // cx represents the caller Realm.
552   RootedObject scope(cx, js::CheckedUnwrapDynamic(&vobj.toObject(), cx));
553   if (!scope) {
554     JS_ReportErrorASCII(
555         cx, "Permission denied to create object in the target scope");
556     return false;
557   }
558 
559   bool define = !JSID_IS_VOID(options.defineAs);
560 
561   if (define && js::IsScriptedProxy(scope)) {
562     JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
563     return false;
564   }
565 
566   RootedObject obj(cx);
567   {
568     JSAutoRealm ar(cx, scope);
569     JS_MarkCrossZoneId(cx, options.defineAs);
570 
571     obj = JS_NewPlainObject(cx);
572     if (!obj) {
573       return false;
574     }
575 
576     if (define) {
577       if (!JS_DefinePropertyById(cx, scope, options.defineAs, obj,
578                                  JSPROP_ENUMERATE))
579         return false;
580     }
581   }
582 
583   rval.setObject(*obj);
584   if (!WrapperFactory::WaiveXrayAndWrap(cx, rval)) {
585     return false;
586   }
587 
588   return true;
589 }
590 
591 } /* namespace xpc */
592