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