Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /* vim: set ts=8 sts=4 et sw=4 tw=99: */
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 "jswrapper.h"
12 : #include "js/Proxy.h"
13 : #include "mozilla/dom/BindingUtils.h"
14 : #include "mozilla/dom/BlobBinding.h"
15 : #include "mozilla/dom/File.h"
16 : #include "mozilla/dom/FileListBinding.h"
17 : #include "mozilla/dom/StructuredCloneHolder.h"
18 : #include "nsGlobalWindow.h"
19 : #include "nsJSUtils.h"
20 : #include "nsIDOMFileList.h"
21 :
22 : using namespace mozilla;
23 : using namespace mozilla::dom;
24 : using namespace JS;
25 :
26 : namespace xpc {
27 :
28 : bool
29 1 : IsReflector(JSObject* obj)
30 : {
31 1 : obj = js::CheckedUnwrap(obj, /* stopAtWindowProxy = */ false);
32 1 : if (!obj)
33 0 : return false;
34 1 : return IS_WN_REFLECTOR(obj) || dom::IsDOMObject(obj);
35 : }
36 :
37 : enum StackScopedCloneTags {
38 : SCTAG_BASE = JS_SCTAG_USER_MIN,
39 : SCTAG_REFLECTOR,
40 : SCTAG_BLOB,
41 : SCTAG_FUNCTION,
42 : };
43 :
44 : // The HTML5 structured cloning algorithm includes a few DOM objects, notably
45 : // FileList. That wouldn't in itself be a reason to support them here,
46 : // but we've historically supported them for Cu.cloneInto (where we didn't support
47 : // other reflectors), so we need to continue to do so in the wrapReflectors == false
48 : // case to maintain compatibility.
49 : //
50 : // FileList clones are supposed to give brand new objects, rather than
51 : // cross-compartment wrappers. For this, our current implementation relies on the
52 : // fact that these objects are implemented with XPConnect and have one reflector
53 : // per scope.
54 0 : bool IsFileList(JSObject* obj)
55 : {
56 0 : return IS_INSTANCE_OF(FileList, obj);
57 : }
58 :
59 : class MOZ_STACK_CLASS StackScopedCloneData
60 : : public StructuredCloneHolderBase
61 : {
62 : public:
63 4 : StackScopedCloneData(JSContext* aCx, StackScopedCloneOptions* aOptions)
64 4 : : mOptions(aOptions)
65 : , mReflectors(aCx)
66 4 : , mFunctions(aCx)
67 4 : {}
68 :
69 4 : ~StackScopedCloneData()
70 8 : {
71 4 : Clear();
72 4 : }
73 :
74 0 : JSObject* CustomReadHandler(JSContext* aCx,
75 : JSStructuredCloneReader* aReader,
76 : uint32_t aTag,
77 : uint32_t aData)
78 : {
79 0 : if (aTag == SCTAG_REFLECTOR) {
80 0 : MOZ_ASSERT(!aData);
81 :
82 : size_t idx;
83 0 : if (!JS_ReadBytes(aReader, &idx, sizeof(size_t)))
84 0 : return nullptr;
85 :
86 0 : RootedObject reflector(aCx, mReflectors[idx]);
87 0 : MOZ_ASSERT(reflector, "No object pointer?");
88 0 : MOZ_ASSERT(IsReflector(reflector), "Object pointer must be a reflector!");
89 :
90 0 : if (!JS_WrapObject(aCx, &reflector))
91 0 : return nullptr;
92 :
93 0 : return reflector;
94 : }
95 :
96 0 : if (aTag == SCTAG_FUNCTION) {
97 0 : MOZ_ASSERT(aData < mFunctions.length());
98 :
99 0 : RootedValue functionValue(aCx);
100 0 : RootedObject obj(aCx, mFunctions[aData]);
101 :
102 0 : if (!JS_WrapObject(aCx, &obj))
103 0 : return nullptr;
104 :
105 0 : FunctionForwarderOptions forwarderOptions;
106 0 : if (!xpc::NewFunctionForwarder(aCx, JSID_VOIDHANDLE, obj, forwarderOptions,
107 : &functionValue))
108 : {
109 0 : return nullptr;
110 : }
111 :
112 0 : return &functionValue.toObject();
113 : }
114 :
115 0 : if (aTag == SCTAG_BLOB) {
116 0 : MOZ_ASSERT(!aData);
117 :
118 : size_t idx;
119 0 : if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
120 0 : return nullptr;
121 : }
122 :
123 0 : nsIGlobalObject* global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
124 0 : MOZ_ASSERT(global);
125 :
126 : // RefPtr<File> needs to go out of scope before toObjectOrNull() is called because
127 : // otherwise the static analysis thinks it can gc the JSObject via the stack.
128 0 : JS::Rooted<JS::Value> val(aCx);
129 : {
130 0 : RefPtr<Blob> blob = Blob::Create(global, mBlobImpls[idx]);
131 0 : if (!ToJSValue(aCx, blob, &val)) {
132 0 : return nullptr;
133 : }
134 : }
135 :
136 0 : return val.toObjectOrNull();
137 : }
138 :
139 0 : MOZ_ASSERT_UNREACHABLE("Encountered garbage in the clone stream!");
140 : return nullptr;
141 : }
142 :
143 0 : bool CustomWriteHandler(JSContext* aCx,
144 : JSStructuredCloneWriter* aWriter,
145 : JS::Handle<JSObject*> aObj)
146 : {
147 : {
148 0 : JS::Rooted<JSObject*> obj(aCx, aObj);
149 0 : Blob* blob = nullptr;
150 0 : if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
151 0 : BlobImpl* blobImpl = blob->Impl();
152 0 : MOZ_ASSERT(blobImpl);
153 :
154 0 : if (!mBlobImpls.AppendElement(blobImpl))
155 0 : return false;
156 :
157 0 : size_t idx = mBlobImpls.Length() - 1;
158 0 : return JS_WriteUint32Pair(aWriter, SCTAG_BLOB, 0) &&
159 0 : JS_WriteBytes(aWriter, &idx, sizeof(size_t));
160 : }
161 : }
162 :
163 0 : if ((mOptions->wrapReflectors && IsReflector(aObj)) ||
164 0 : IsFileList(aObj))
165 : {
166 0 : if (!mReflectors.append(aObj))
167 0 : return false;
168 :
169 0 : size_t idx = mReflectors.length() - 1;
170 0 : if (!JS_WriteUint32Pair(aWriter, SCTAG_REFLECTOR, 0))
171 0 : return false;
172 0 : if (!JS_WriteBytes(aWriter, &idx, sizeof(size_t)))
173 0 : return false;
174 0 : return true;
175 : }
176 :
177 0 : if (JS::IsCallable(aObj)) {
178 0 : if (mOptions->cloneFunctions) {
179 0 : if (!mFunctions.append(aObj))
180 0 : return false;
181 0 : return JS_WriteUint32Pair(aWriter, SCTAG_FUNCTION, mFunctions.length() - 1);
182 : } else {
183 0 : JS_ReportErrorASCII(aCx, "Permission denied to pass a Function via structured clone");
184 0 : return false;
185 : }
186 : }
187 :
188 0 : JS_ReportErrorASCII(aCx, "Encountered unsupported value type writing stack-scoped structured clone");
189 0 : return false;
190 : }
191 :
192 : StackScopedCloneOptions* mOptions;
193 : AutoObjectVector mReflectors;
194 : AutoObjectVector mFunctions;
195 : nsTArray<RefPtr<BlobImpl>> mBlobImpls;
196 : };
197 :
198 : /*
199 : * General-purpose structured-cloning utility for cases where the structured
200 : * clone buffer is only used in stack-scope (that is to say, the buffer does
201 : * not escape from this function). The stack-scoping allows us to pass
202 : * references to various JSObjects directly in certain situations without
203 : * worrying about lifetime issues.
204 : *
205 : * This function assumes that |cx| is already entered the compartment we want
206 : * to clone to, and that |val| may not be same-compartment with cx. When the
207 : * function returns, |val| is set to the result of the clone.
208 : */
209 : bool
210 4 : StackScopedClone(JSContext* cx, StackScopedCloneOptions& options,
211 : MutableHandleValue val)
212 : {
213 8 : StackScopedCloneData data(cx, &options);
214 : {
215 : // For parsing val we have to enter its compartment.
216 : // (unless it's a primitive)
217 8 : Maybe<JSAutoCompartment> ac;
218 4 : if (val.isObject()) {
219 4 : ac.emplace(cx, &val.toObject());
220 0 : } else if (val.isString() && !JS_WrapValue(cx, val)) {
221 0 : return false;
222 : }
223 :
224 4 : if (!data.Write(cx, val))
225 0 : return false;
226 : }
227 :
228 : // Now recreate the clones in the target compartment.
229 4 : if (!data.Read(cx, val))
230 0 : return false;
231 :
232 : // Deep-freeze if requested.
233 4 : if (options.deepFreeze && val.isObject()) {
234 0 : RootedObject obj(cx, &val.toObject());
235 0 : if (!JS_DeepFreezeObject(cx, obj))
236 0 : return false;
237 : }
238 :
239 4 : return true;
240 : }
241 :
242 : // Note - This function mirrors the logic of CheckPassToChrome in
243 : // ChromeObjectWrapper.cpp.
244 : static bool
245 0 : CheckSameOriginArg(JSContext* cx, FunctionForwarderOptions& options, HandleValue v)
246 : {
247 : // Consumers can explicitly opt out of this security check. This is used in
248 : // the web console to allow the utility functions to accept cross-origin Windows.
249 0 : if (options.allowCrossOriginArguments)
250 0 : return true;
251 :
252 : // Primitives are fine.
253 0 : if (!v.isObject())
254 0 : return true;
255 0 : RootedObject obj(cx, &v.toObject());
256 0 : MOZ_ASSERT(js::GetObjectCompartment(obj) != js::GetContextCompartment(cx),
257 : "This should be invoked after entering the compartment but before "
258 : "wrapping the values");
259 :
260 : // Non-wrappers are fine.
261 0 : if (!js::IsWrapper(obj))
262 0 : return true;
263 :
264 : // Wrappers leading back to the scope of the exported function are fine.
265 0 : if (js::GetObjectCompartment(js::UncheckedUnwrap(obj)) == js::GetContextCompartment(cx))
266 0 : return true;
267 :
268 : // Same-origin wrappers are fine.
269 0 : if (AccessCheck::wrapperSubsumes(obj))
270 0 : return true;
271 :
272 : // Badness.
273 0 : JS_ReportErrorASCII(cx, "Permission denied to pass object to exported function");
274 0 : return false;
275 : }
276 :
277 : static bool
278 0 : FunctionForwarder(JSContext* cx, unsigned argc, Value* vp)
279 : {
280 0 : CallArgs args = CallArgsFromVp(argc, vp);
281 :
282 : // Grab the options from the reserved slot.
283 0 : RootedObject optionsObj(cx, &js::GetFunctionNativeReserved(&args.callee(), 1).toObject());
284 0 : FunctionForwarderOptions options(cx, optionsObj);
285 0 : if (!options.Parse())
286 0 : return false;
287 :
288 : // Grab and unwrap the underlying callable.
289 0 : RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
290 0 : RootedObject unwrappedFun(cx, js::UncheckedUnwrap(&v.toObject()));
291 :
292 0 : RootedObject thisObj(cx, args.isConstructing() ? nullptr : JS_THIS_OBJECT(cx, vp));
293 : {
294 : // We manually implement the contents of CrossCompartmentWrapper::call
295 : // here, because certain function wrappers (notably content->nsEP) are
296 : // not callable.
297 0 : JSAutoCompartment ac(cx, unwrappedFun);
298 :
299 0 : RootedValue thisVal(cx, ObjectOrNullValue(thisObj));
300 0 : if (!CheckSameOriginArg(cx, options, thisVal) || !JS_WrapObject(cx, &thisObj))
301 0 : return false;
302 :
303 0 : for (size_t n = 0; n < args.length(); ++n) {
304 0 : if (!CheckSameOriginArg(cx, options, args[n]) || !JS_WrapValue(cx, args[n]))
305 0 : return false;
306 : }
307 :
308 0 : RootedValue fval(cx, ObjectValue(*unwrappedFun));
309 0 : if (args.isConstructing()) {
310 0 : RootedObject obj(cx);
311 0 : if (!JS::Construct(cx, fval, args, &obj))
312 0 : return false;
313 0 : args.rval().setObject(*obj);
314 : } else {
315 0 : if (!JS_CallFunctionValue(cx, thisObj, fval, args, args.rval()))
316 0 : return false;
317 : }
318 : }
319 :
320 : // Rewrap the return value into our compartment.
321 0 : return JS_WrapValue(cx, args.rval());
322 : }
323 :
324 : bool
325 0 : NewFunctionForwarder(JSContext* cx, HandleId idArg, HandleObject callable,
326 : FunctionForwarderOptions& options, MutableHandleValue vp)
327 : {
328 0 : RootedId id(cx, idArg);
329 0 : if (id == JSID_VOIDHANDLE)
330 0 : id = GetJSIDByIndex(cx, XPCJSContext::IDX_EMPTYSTRING);
331 :
332 : // We have no way of knowing whether the underlying function wants to be a
333 : // constructor or not, so we just mark all forwarders as constructors, and
334 : // let the underlying function throw for construct calls if it wants.
335 0 : JSFunction* fun = js::NewFunctionByIdWithReserved(cx, FunctionForwarder,
336 0 : 0, JSFUN_CONSTRUCTOR, id);
337 0 : if (!fun)
338 0 : return false;
339 :
340 : // Stash the callable in slot 0.
341 0 : AssertSameCompartment(cx, callable);
342 0 : RootedObject funobj(cx, JS_GetFunctionObject(fun));
343 0 : js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
344 :
345 : // Stash the options in slot 1.
346 0 : RootedObject optionsObj(cx, options.ToJSObject(cx));
347 0 : if (!optionsObj)
348 0 : return false;
349 0 : js::SetFunctionNativeReserved(funobj, 1, ObjectValue(*optionsObj));
350 :
351 0 : vp.setObject(*funobj);
352 0 : return true;
353 : }
354 :
355 : bool
356 0 : ExportFunction(JSContext* cx, HandleValue vfunction, HandleValue vscope, HandleValue voptions,
357 : MutableHandleValue rval)
358 : {
359 0 : bool hasOptions = !voptions.isUndefined();
360 0 : if (!vscope.isObject() || !vfunction.isObject() || (hasOptions && !voptions.isObject())) {
361 0 : JS_ReportErrorASCII(cx, "Invalid argument");
362 0 : return false;
363 : }
364 :
365 0 : RootedObject funObj(cx, &vfunction.toObject());
366 0 : RootedObject targetScope(cx, &vscope.toObject());
367 0 : ExportFunctionOptions options(cx, hasOptions ? &voptions.toObject() : nullptr);
368 0 : if (hasOptions && !options.Parse())
369 0 : return false;
370 :
371 : // Restrictions:
372 : // * We must subsume the scope we are exporting to.
373 : // * We must subsume the function being exported, because the function
374 : // forwarder manually circumvents security wrapper CALL restrictions.
375 0 : targetScope = js::CheckedUnwrap(targetScope);
376 0 : funObj = js::CheckedUnwrap(funObj);
377 0 : if (!targetScope || !funObj) {
378 0 : JS_ReportErrorASCII(cx, "Permission denied to export function into scope");
379 0 : return false;
380 : }
381 :
382 0 : if (js::IsScriptedProxy(targetScope)) {
383 0 : JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
384 0 : return false;
385 : }
386 :
387 : {
388 : // We need to operate in the target scope from here on, let's enter
389 : // its compartment.
390 0 : JSAutoCompartment ac(cx, targetScope);
391 :
392 : // Unwrapping to see if we have a callable.
393 0 : funObj = UncheckedUnwrap(funObj);
394 0 : if (!JS::IsCallable(funObj)) {
395 0 : JS_ReportErrorASCII(cx, "First argument must be a function");
396 0 : return false;
397 : }
398 :
399 0 : RootedId id(cx, options.defineAs);
400 0 : if (JSID_IS_VOID(id)) {
401 : // If there wasn't any function name specified,
402 : // copy the name from the function being imported.
403 0 : JSFunction* fun = JS_GetObjectFunction(funObj);
404 0 : RootedString funName(cx, JS_GetFunctionId(fun));
405 0 : if (!funName)
406 0 : funName = JS_AtomizeAndPinString(cx, "");
407 0 : JS_MarkCrossZoneIdValue(cx, StringValue(funName));
408 :
409 0 : if (!JS_StringToId(cx, funName, &id))
410 0 : return false;
411 : } else {
412 0 : JS_MarkCrossZoneId(cx, id);
413 : }
414 0 : MOZ_ASSERT(JSID_IS_STRING(id));
415 :
416 : // The function forwarder will live in the target compartment. Since
417 : // this function will be referenced from its private slot, to avoid a
418 : // GC hazard, we must wrap it to the same compartment.
419 0 : if (!JS_WrapObject(cx, &funObj))
420 0 : return false;
421 :
422 : // And now, let's create the forwarder function in the target compartment
423 : // for the function the be exported.
424 0 : FunctionForwarderOptions forwarderOptions;
425 0 : forwarderOptions.allowCrossOriginArguments = options.allowCrossOriginArguments;
426 0 : if (!NewFunctionForwarder(cx, id, funObj, forwarderOptions, rval)) {
427 0 : JS_ReportErrorASCII(cx, "Exporting function failed");
428 0 : return false;
429 : }
430 :
431 : // We have the forwarder function in the target compartment. If
432 : // defineAs was set, we also need to define it as a property on
433 : // the target.
434 0 : if (!JSID_IS_VOID(options.defineAs)) {
435 0 : if (!JS_DefinePropertyById(cx, targetScope, id, rval,
436 : JSPROP_ENUMERATE,
437 : JS_STUBGETTER, JS_STUBSETTER)) {
438 0 : return false;
439 : }
440 : }
441 : }
442 :
443 : // Finally we have to re-wrap the exported function back to the caller compartment.
444 0 : if (!JS_WrapValue(cx, rval))
445 0 : return false;
446 :
447 0 : return true;
448 : }
449 :
450 : bool
451 0 : CreateObjectIn(JSContext* cx, HandleValue vobj, CreateObjectInOptions& options,
452 : MutableHandleValue rval)
453 : {
454 0 : if (!vobj.isObject()) {
455 0 : JS_ReportErrorASCII(cx, "Expected an object as the target scope");
456 0 : return false;
457 : }
458 :
459 0 : RootedObject scope(cx, js::CheckedUnwrap(&vobj.toObject()));
460 0 : if (!scope) {
461 0 : JS_ReportErrorASCII(cx, "Permission denied to create object in the target scope");
462 0 : return false;
463 : }
464 :
465 0 : bool define = !JSID_IS_VOID(options.defineAs);
466 :
467 0 : if (define && js::IsScriptedProxy(scope)) {
468 0 : JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed");
469 0 : return false;
470 : }
471 :
472 0 : RootedObject obj(cx);
473 : {
474 0 : JSAutoCompartment ac(cx, scope);
475 0 : JS_MarkCrossZoneId(cx, options.defineAs);
476 :
477 0 : obj = JS_NewPlainObject(cx);
478 0 : if (!obj)
479 0 : return false;
480 :
481 0 : if (define) {
482 0 : if (!JS_DefinePropertyById(cx, scope, options.defineAs, obj,
483 : JSPROP_ENUMERATE,
484 : JS_STUBGETTER, JS_STUBSETTER))
485 0 : return false;
486 : }
487 : }
488 :
489 0 : rval.setObject(*obj);
490 0 : if (!WrapperFactory::WaiveXrayAndWrap(cx, rval))
491 0 : return false;
492 :
493 0 : return true;
494 : }
495 :
496 : } /* namespace xpc */
|