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