WrapPrivileged.sys.mjs (11547B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This module handles wrapping privileged objects so that they can be exposed 7 * to unprivileged contexts. It is only to be used in automated tests. 8 * 9 * Its exact semantics are also liable to change at any time, so any callers 10 * relying on undocumented behavior or subtle platform features should expect 11 * breakage. Those callers should, wherever possible, migrate to fully 12 * chrome-privileged scripts when they need to interact with privileged APIs. 13 */ 14 15 // XPCNativeWrapper is not defined globally in ESLint as it may be going away. 16 // See bug 1481337. 17 /* globals XPCNativeWrapper */ 18 19 Cu.crashIfNotInAutomation(); 20 21 let wrappedObjects = new WeakMap(); 22 let perWindowInfo = new WeakMap(); 23 let noAutoWrap = new WeakSet(); 24 25 function isWrappable(x) { 26 if (typeof x === "object") { 27 return x !== null; 28 } 29 return typeof x === "function"; 30 } 31 32 function isWrapper(x) { 33 try { 34 return isWrappable(x) && wrappedObjects.has(x); 35 } catch (e) { 36 // If `x` is a remote object proxy, trying to access an unexpected property 37 // on it will throw a security error, even though we're chrome privileged. 38 // However, remote proxies are not SpecialPowers wrappers, so: 39 return false; 40 } 41 } 42 43 function unwrapIfWrapped(x) { 44 return isWrapper(x) ? unwrapPrivileged(x) : x; 45 } 46 47 function wrapIfUnwrapped(x, w) { 48 return isWrapper(x) ? x : wrapPrivileged(x, w); 49 } 50 51 function isObjectOrArray(obj) { 52 if (Object(obj) !== obj) { 53 return false; 54 } 55 let arrayClasses = [ 56 "Object", 57 "Array", 58 "Int8Array", 59 "Uint8Array", 60 "Int16Array", 61 "Uint16Array", 62 "Int32Array", 63 "Uint32Array", 64 "Float32Array", 65 "Float64Array", 66 "Uint8ClampedArray", 67 ]; 68 let className = Cu.getClassName(obj, true); 69 return arrayClasses.includes(className); 70 } 71 72 // In general, we want Xray wrappers for content DOM objects, because waiving 73 // Xray gives us Xray waiver wrappers that clamp the principal when we cross 74 // compartment boundaries. However, there are some exceptions where we want 75 // to use a waiver: 76 // 77 // * Xray adds some gunk to toString(), which has the potential to confuse 78 // consumers that aren't expecting Xray wrappers. Since toString() is a 79 // non-privileged method that returns only strings, we can just waive Xray 80 // for that case. 81 // 82 // * We implement Xrays to pure JS [[Object]] and [[Array]] instances that 83 // filter out tricky things like callables. This is the right thing for 84 // security in general, but tends to break tests that try to pass object 85 // literals into SpecialPowers. So we waive [[Object]] and [[Array]] 86 // instances before inspecting properties. 87 // 88 // * When we don't have meaningful Xray semantics, we create an Opaque 89 // XrayWrapper for security reasons. For test code, we generally want to see 90 // through that sort of thing. 91 function waiveXraysIfAppropriate(obj, propName) { 92 if ( 93 propName == "toString" || 94 isObjectOrArray(obj) || 95 /Opaque/.test(Object.prototype.toString.call(obj)) 96 ) { 97 return XPCNativeWrapper.unwrap(obj); 98 } 99 return obj; 100 } 101 102 // We can't call apply() directy on Xray-wrapped functions, so we have to be 103 // clever. 104 function doApply(fun, invocant, args) { 105 // We implement Xrays to pure JS [[Object]] instances that filter out tricky 106 // things like callables. This is the right thing for security in general, 107 // but tends to break tests that try to pass object literals into 108 // SpecialPowers. So we waive [[Object]] instances when they're passed to a 109 // SpecialPowers-wrapped callable. 110 // 111 // Note that the transitive nature of Xray waivers means that any property 112 // pulled off such an object will also be waived, and so we'll get principal 113 // clamping for Xrayed DOM objects reached from literals, so passing things 114 // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't 115 // go that deep. 116 args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x)); 117 return Reflect.apply(fun, invocant, args); 118 } 119 120 function wrapPrivileged(obj, win) { 121 // Primitives pass straight through. 122 if (!isWrappable(obj)) { 123 return obj; 124 } 125 126 // No double wrapping. 127 if (isWrapper(obj)) { 128 throw new Error("Trying to double-wrap object!"); 129 } 130 131 let { windowID, proxies, handler } = perWindowInfo.get(win) || {}; 132 // |windowUtils| is undefined if |win| is a non-window object 133 // such as a sandbox. 134 let currentID = win.windowGlobalChild 135 ? win.windowGlobalChild.innerWindowId 136 : 0; 137 // Values are dead objects if the inner window is changed. 138 if (windowID !== currentID) { 139 windowID = currentID; 140 proxies = new WeakMap(); 141 handler = Cu.cloneInto(SpecialPowersHandler, win, { 142 cloneFunctions: true, 143 }); 144 handler.wrapped = new win.WeakMap(); 145 perWindowInfo.set(win, { windowID, proxies, handler }); 146 } 147 148 if (proxies.has(obj)) { 149 return proxies.get(obj).proxy; 150 } 151 152 let className = Cu.getClassName(obj, true); 153 if (className === "ArrayBuffer") { 154 // Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to 155 // return a real ArrayBuffer. 156 return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win); 157 } 158 159 let dummy; 160 if (typeof obj === "function") { 161 dummy = Cu.exportFunction(function () {}, win); 162 } else { 163 dummy = new win.Object(); 164 } 165 handler.wrapped.set(dummy, { obj }); 166 167 let proxy = new win.Proxy(dummy, handler); 168 wrappedObjects.set(proxy, obj); 169 switch (className) { 170 case "AnonymousContent": 171 // Caching anonymous content will cause crashes (bug 1636015). 172 break; 173 case "CSSStyleProperties": 174 case "CSSStyleRule": 175 case "CSSStyleSheet": 176 // Caching these classes will cause memory leaks. 177 break; 178 default: 179 proxies.set(obj, { proxy }); 180 break; 181 } 182 return proxy; 183 } 184 185 function unwrapPrivileged(x) { 186 // We don't wrap primitives, so sometimes we have a primitive where we'd 187 // expect to have a wrapper. The proxy pretends to be the type that it's 188 // emulating, so we can just as easily check isWrappable() on a proxy as 189 // we can on an unwrapped object. 190 if (!isWrappable(x)) { 191 return x; 192 } 193 194 // If we have a wrappable type, make sure it's wrapped. 195 if (!isWrapper(x)) { 196 throw new Error("Trying to unwrap a non-wrapped object!"); 197 } 198 199 // unwrapped. 200 return wrappedObjects.get(x); 201 } 202 203 function wrapExceptions(global, fn) { 204 try { 205 return fn(); 206 } catch (e) { 207 throw wrapIfUnwrapped(e, global); 208 } 209 } 210 211 let SpecialPowersHandler = { 212 construct(target, args) { 213 // The arguments may or may not be wrappers. Unwrap them if necessary. 214 var unwrappedArgs = Array.from(Cu.waiveXrays(args), x => 215 unwrapIfWrapped(Cu.unwaiveXrays(x)) 216 ); 217 218 // We want to invoke "obj" as a constructor, but using unwrappedArgs as 219 // the arguments. 220 let global = Cu.getGlobalForObject(this); 221 return wrapExceptions(global, () => 222 wrapIfUnwrapped( 223 Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs), 224 global 225 ) 226 ); 227 }, 228 229 apply(target, thisValue, args) { 230 let wrappedObject = this.wrapped.get(target).obj; 231 let global = Cu.getGlobalForObject(this); 232 // The invocant and arguments may or may not be wrappers. Unwrap 233 // them if necessary. 234 var invocant = unwrapIfWrapped(thisValue); 235 236 return wrapExceptions(global, () => { 237 if (noAutoWrap.has(wrappedObject)) { 238 args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x)); 239 return doApply(wrappedObject, invocant, args); 240 } 241 242 if (wrappedObject.name == "then") { 243 args = Array.from(Cu.waiveXrays(args), x => 244 wrapCallback(Cu.unwaiveXrays(x), global) 245 ); 246 } else { 247 args = Array.from(Cu.waiveXrays(args), x => 248 unwrapIfWrapped(Cu.unwaiveXrays(x)) 249 ); 250 } 251 252 return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global); 253 }); 254 }, 255 256 has(target, prop) { 257 return Reflect.has(this.wrapped.get(target).obj, prop); 258 }, 259 260 get(target, prop) { 261 let global = Cu.getGlobalForObject(this); 262 return wrapExceptions(global, () => { 263 let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop); 264 let val = Reflect.get(obj, prop); 265 return wrapIfUnwrapped(val, global); 266 }); 267 }, 268 269 set(target, prop, val) { 270 return wrapExceptions(Cu.getGlobalForObject(this), () => { 271 let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop); 272 return Reflect.set(obj, prop, unwrapIfWrapped(val)); 273 }); 274 }, 275 276 delete(target, prop) { 277 return wrapExceptions(Cu.getGlobalForObject(this), () => { 278 return Reflect.deleteProperty(this.wrapped.get(target).obj, prop); 279 }); 280 }, 281 282 defineProperty() { 283 throw new Error( 284 "Can't call defineProperty on SpecialPowers wrapped object" 285 ); 286 }, 287 288 getOwnPropertyDescriptor(target, prop) { 289 let global = Cu.getGlobalForObject(this); 290 return wrapExceptions(global, () => { 291 let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop); 292 let desc = Reflect.getOwnPropertyDescriptor(obj, prop); 293 294 if (desc === undefined) { 295 return undefined; 296 } 297 298 // Transitively maintain the wrapper membrane. 299 let wrapIfExists = key => { 300 if (key in desc) { 301 desc[key] = wrapIfUnwrapped(desc[key], global); 302 } 303 }; 304 305 wrapIfExists("value"); 306 wrapIfExists("get"); 307 wrapIfExists("set"); 308 309 // A trapping proxy's properties must always be configurable, but sometimes 310 // we come across non-configurable properties. Tell a white lie. 311 desc.configurable = true; 312 313 return wrapIfUnwrapped(desc, global); 314 }); 315 }, 316 317 ownKeys(target) { 318 let props = []; 319 320 // Do the normal thing. 321 let wrappedObject = this.wrapped.get(target).obj; 322 let flt = a => !props.includes(a); 323 props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt)); 324 325 // If we've got an Xray wrapper, include the expandos as well. 326 if ("wrappedJSObject" in wrappedObject) { 327 props = props.concat( 328 Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt) 329 ); 330 } 331 332 return Cu.cloneInto(props, Cu.getGlobalForObject(this)); 333 }, 334 335 preventExtensions() { 336 throw new Error( 337 "Can't call preventExtensions on SpecialPowers wrapped object" 338 ); 339 }, 340 }; 341 342 function wrapCallback(cb, win) { 343 // Do not wrap if it is already privileged. 344 if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) { 345 return cb; 346 } 347 return function SpecialPowersCallbackWrapper() { 348 var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win)); 349 let invocant = wrapIfUnwrapped(this, win); 350 return unwrapIfWrapped(cb.apply(invocant, args)); 351 }; 352 } 353 354 function wrapCallbackObject(obj, win) { 355 // Do not wrap if it is already privileged. 356 if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) { 357 return obj; 358 } 359 obj = Cu.waiveXrays(obj); 360 var wrapper = {}; 361 for (var i in obj) { 362 if (typeof obj[i] == "function") { 363 wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win); 364 } else { 365 wrapper[i] = obj[i]; 366 } 367 } 368 return wrapper; 369 } 370 371 function disableAutoWrap(...objs) { 372 objs.forEach(x => noAutoWrap.add(x)); 373 } 374 375 export var WrapPrivileged = { 376 wrap: wrapIfUnwrapped, 377 unwrap: unwrapIfWrapped, 378 379 isWrapper, 380 381 wrapCallback, 382 wrapCallbackObject, 383 384 disableAutoWrap, 385 };