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