sandbox.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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. 'use strict';
  5. module.metadata = {
  6. 'stability': 'unstable'
  7. };
  8. const { Class } = require('../core/heritage');
  9. const { EventTarget } = require('../event/target');
  10. const { on, off, emit } = require('../event/core');
  11. const {
  12. requiresAddonGlobal,
  13. attach, detach, destroy
  14. } = require('./utils');
  15. const { delay: async } = require('../lang/functional');
  16. const { Ci, Cu, Cc } = require('chrome');
  17. const timer = require('../timers');
  18. const { URL } = require('../url');
  19. const { sandbox, evaluate, load } = require('../loader/sandbox');
  20. const { merge } = require('../util/object');
  21. const xulApp = require('../system/xul-app');
  22. const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
  23. '17.0a2', '*');
  24. const { getTabForContentWindow } = require('../tabs/utils');
  25. // WeakMap of sandboxes so we can access private values
  26. const sandboxes = new WeakMap();
  27. /* Trick the linker in order to ensure shipping these files in the XPI.
  28. require('./content-worker.js');
  29. Then, retrieve URL of these files in the XPI:
  30. */
  31. let prefix = module.uri.split('sandbox.js')[0];
  32. const CONTENT_WORKER_URL = prefix + 'content-worker.js';
  33. // Fetch additional list of domains to authorize access to for each content
  34. // script. It is stored in manifest `metadata` field which contains
  35. // package.json data. This list is originaly defined by authors in
  36. // `permissions` attribute of their package.json addon file.
  37. const permissions = require('@loader/options').metadata['permissions'] || {};
  38. const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
  39. const JS_VERSION = '1.8';
  40. const WorkerSandbox = Class({
  41. implements: [
  42. EventTarget
  43. ],
  44. /**
  45. * Emit a message to the worker content sandbox
  46. */
  47. emit: function emit(...args) {
  48. // Ensure having an asynchronous behavior
  49. let self = this;
  50. async(function () {
  51. emitToContent(self, JSON.stringify(args, replacer));
  52. });
  53. },
  54. /**
  55. * Synchronous version of `emit`.
  56. * /!\ Should only be used when it is strictly mandatory /!\
  57. * Doesn't ensure passing only JSON values.
  58. * Mainly used by context-menu in order to avoid breaking it.
  59. */
  60. emitSync: function emitSync(...args) {
  61. return emitToContent(this, args);
  62. },
  63. /**
  64. * Tells if content script has at least one listener registered for one event,
  65. * through `self.on('xxx', ...)`.
  66. * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
  67. */
  68. hasListenerFor: function hasListenerFor(name) {
  69. return modelFor(this).hasListenerFor(name);
  70. },
  71. /**
  72. * Configures sandbox and loads content scripts into it.
  73. * @param {Worker} worker
  74. * content worker
  75. */
  76. initialize: function WorkerSandbox(worker, window) {
  77. let model = {};
  78. sandboxes.set(this, model);
  79. model.worker = worker;
  80. // We receive a wrapped window, that may be an xraywrapper if it's content
  81. let proto = window;
  82. // TODO necessary?
  83. // Ensure that `emit` has always the right `this`
  84. this.emit = this.emit.bind(this);
  85. this.emitSync = this.emitSync.bind(this);
  86. // Eventually use expanded principal sandbox feature, if some are given.
  87. //
  88. // But prevent it when the Worker isn't used for a content script but for
  89. // injecting `addon` object into a Panel, Widget, ... scope.
  90. // That's because:
  91. // 1/ It is useless to use multiple domains as the worker is only used
  92. // to communicate with the addon,
  93. // 2/ By using it it would prevent the document to have access to any JS
  94. // value of the worker. As JS values coming from multiple domain principals
  95. // can't be accessed by 'mono-principals' (principal with only one domain).
  96. // Even if this principal is for a domain that is specified in the multiple
  97. // domain principal.
  98. let principals = window;
  99. let wantGlobalProperties = [];
  100. if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) {
  101. principals = EXPANDED_PRINCIPALS.concat(window);
  102. // We have to replace XHR constructor of the content document
  103. // with a custom cross origin one, automagically added by platform code:
  104. delete proto.XMLHttpRequest;
  105. wantGlobalProperties.push('XMLHttpRequest');
  106. }
  107. // Instantiate trusted code in another Sandbox in order to prevent content
  108. // script from messing with standard classes used by proxy and API code.
  109. let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
  110. apiSandbox.console = console;
  111. // Create the sandbox and bind it to window in order for content scripts to
  112. // have access to all standard globals (window, document, ...)
  113. let content = sandbox(principals, {
  114. sandboxPrototype: proto,
  115. wantXrays: true,
  116. wantGlobalProperties: wantGlobalProperties,
  117. sameZoneAs: window,
  118. metadata: { SDKContentScript: true }
  119. });
  120. model.sandbox = content;
  121. // We have to ensure that window.top and window.parent are the exact same
  122. // object than window object, i.e. the sandbox global object. But not
  123. // always, in case of iframes, top and parent are another window object.
  124. let top = window.top === window ? content : content.top;
  125. let parent = window.parent === window ? content : content.parent;
  126. merge(content, {
  127. // We need 'this === window === top' to be true in toplevel scope:
  128. get window() content,
  129. get top() top,
  130. get parent() parent,
  131. // Use the Greasemonkey naming convention to provide access to the
  132. // unwrapped window object so the content script can access document
  133. // JavaScript values.
  134. // NOTE: this functionality is experimental and may change or go away
  135. // at any time!
  136. get unsafeWindow() window.wrappedJSObject
  137. });
  138. // Load trusted code that will inject content script API.
  139. // We need to expose JS objects defined in same principal in order to
  140. // avoid having any kind of wrapper.
  141. load(apiSandbox, CONTENT_WORKER_URL);
  142. // prepare a clean `self.options`
  143. let options = 'contentScriptOptions' in worker ?
  144. JSON.stringify(worker.contentScriptOptions) :
  145. undefined;
  146. // Then call `inject` method and communicate with this script
  147. // by trading two methods that allow to send events to the other side:
  148. // - `onEvent` called by content script
  149. // - `result.emitToContent` called by addon script
  150. // Bug 758203: We have to explicitely define `__exposedProps__` in order
  151. // to allow access to these chrome object attributes from this sandbox with
  152. // content priviledges
  153. // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
  154. let onEvent = onContentEvent.bind(null, this);
  155. // `ContentWorker` is defined in CONTENT_WORKER_URL file
  156. let chromeAPI = createChromeAPI();
  157. let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
  158. // Merge `emitToContent` and `hasListenerFor` into our private
  159. // model of the WorkerSandbox so we can communicate with content
  160. // script
  161. merge(model, result);
  162. // Handle messages send by this script:
  163. setListeners(this);
  164. // Inject `addon` global into target document if document is trusted,
  165. // `addon` in document is equivalent to `self` in content script.
  166. if (requiresAddonGlobal(worker)) {
  167. Object.defineProperty(getUnsafeWindow(window), 'addon', {
  168. value: content.self
  169. }
  170. );
  171. }
  172. // Inject our `console` into target document if worker doesn't have a tab
  173. // (e.g Panel, PageWorker, Widget).
  174. // `worker.tab` can't be used because bug 804935.
  175. if (!getTabForContentWindow(window)) {
  176. let win = getUnsafeWindow(window);
  177. // export our chrome console to content window, using the same approach
  178. // of `ConsoleAPI`:
  179. // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
  180. //
  181. // and described here:
  182. // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
  183. let con = Cu.createObjectIn(win);
  184. let genPropDesc = function genPropDesc(fun) {
  185. return { enumerable: true, configurable: true, writable: true,
  186. value: console[fun] };
  187. }
  188. const properties = {
  189. log: genPropDesc('log'),
  190. info: genPropDesc('info'),
  191. warn: genPropDesc('warn'),
  192. error: genPropDesc('error'),
  193. debug: genPropDesc('debug'),
  194. trace: genPropDesc('trace'),
  195. dir: genPropDesc('dir'),
  196. group: genPropDesc('group'),
  197. groupCollapsed: genPropDesc('groupCollapsed'),
  198. groupEnd: genPropDesc('groupEnd'),
  199. time: genPropDesc('time'),
  200. timeEnd: genPropDesc('timeEnd'),
  201. profile: genPropDesc('profile'),
  202. profileEnd: genPropDesc('profileEnd'),
  203. __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
  204. value: function() {} }
  205. };
  206. Object.defineProperties(con, properties);
  207. Cu.makeObjectPropsNormal(con);
  208. win.console = con;
  209. };
  210. // The order of `contentScriptFile` and `contentScript` evaluation is
  211. // intentional, so programs can load libraries like jQuery from script URLs
  212. // and use them in scripts.
  213. let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
  214. : null,
  215. contentScript = ('contentScript' in worker) ? worker.contentScript : null;
  216. if (contentScriptFile)
  217. importScripts.apply(null, [this].concat(contentScriptFile));
  218. if (contentScript) {
  219. evaluateIn(
  220. this,
  221. Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
  222. );
  223. }
  224. },
  225. destroy: function destroy() {
  226. this.emitSync('detach');
  227. let model = modelFor(this);
  228. model.sandbox = null
  229. model.worker = null;
  230. },
  231. });
  232. exports.WorkerSandbox = WorkerSandbox;
  233. /**
  234. * Imports scripts to the sandbox by reading files under urls and
  235. * evaluating its source. If exception occurs during evaluation
  236. * `'error'` event is emitted on the worker.
  237. * This is actually an analog to the `importScript` method in web
  238. * workers but in our case it's not exposed even though content
  239. * scripts may be able to do it synchronously since IO operation
  240. * takes place in the UI process.
  241. */
  242. function importScripts (workerSandbox, ...urls) {
  243. let { worker, sandbox } = modelFor(workerSandbox);
  244. for (let i in urls) {
  245. let contentScriptFile = urls[i];
  246. try {
  247. let uri = URL(contentScriptFile);
  248. if (uri.scheme === 'resource')
  249. load(sandbox, String(uri));
  250. else
  251. throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
  252. }
  253. catch(e) {
  254. emit(worker, 'error', e);
  255. }
  256. }
  257. }
  258. function setListeners (workerSandbox) {
  259. let { worker } = modelFor(workerSandbox);
  260. // console.xxx calls
  261. workerSandbox.on('console', function consoleListener (kind, ...args) {
  262. console[kind].apply(console, args);
  263. });
  264. // self.postMessage calls
  265. workerSandbox.on('message', function postMessage(data) {
  266. // destroyed?
  267. if (worker)
  268. emit(worker, 'message', data);
  269. });
  270. // self.port.emit calls
  271. workerSandbox.on('event', function portEmit (...eventArgs) {
  272. // If not destroyed, emit event information to worker
  273. // `eventArgs` has the event name as first element,
  274. // and remaining elements are additional arguments to pass
  275. if (worker)
  276. emit.apply(null, [worker.port].concat(eventArgs));
  277. });
  278. // unwrap, recreate and propagate async Errors thrown from content-script
  279. workerSandbox.on('error', function onError({instanceOfError, value}) {
  280. if (worker) {
  281. let error = value;
  282. if (instanceOfError) {
  283. error = new Error(value.message, value.fileName, value.lineNumber);
  284. error.stack = value.stack;
  285. error.name = value.name;
  286. }
  287. emit(worker, 'error', error);
  288. }
  289. });
  290. }
  291. /**
  292. * Evaluates code in the sandbox.
  293. * @param {String} code
  294. * JavaScript source to evaluate.
  295. * @param {String} [filename='javascript:' + code]
  296. * Name of the file
  297. */
  298. function evaluateIn (workerSandbox, code, filename) {
  299. let { worker, sandbox } = modelFor(workerSandbox);
  300. try {
  301. evaluate(sandbox, code, filename || 'javascript:' + code);
  302. }
  303. catch(e) {
  304. emit(worker, 'error', e);
  305. }
  306. }
  307. /**
  308. * Method called by the worker sandbox when it needs to send a message
  309. */
  310. function onContentEvent (workerSandbox, args) {
  311. // As `emit`, we ensure having an asynchronous behavior
  312. async(function () {
  313. // We emit event to chrome/addon listeners
  314. emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
  315. });
  316. }
  317. function modelFor (workerSandbox) {
  318. return sandboxes.get(workerSandbox);
  319. }
  320. /**
  321. * JSON.stringify is buggy with cross-sandbox values,
  322. * it may return '{}' on functions. Use a replacer to match them correctly.
  323. */
  324. function replacer (k, v) {
  325. return typeof v === 'function' ? undefined : v;
  326. }
  327. function getUnsafeWindow (win) {
  328. return win.wrappedJSObject || win;
  329. }
  330. function emitToContent (workerSandbox, args) {
  331. return modelFor(workerSandbox).emitToContent(args);
  332. }
  333. function createChromeAPI () {
  334. return {
  335. timers: {
  336. setTimeout: timer.setTimeout,
  337. setInterval: timer.setInterval,
  338. clearTimeout: timer.clearTimeout,
  339. clearInterval: timer.clearInterval,
  340. __exposedProps__: {
  341. setTimeout: 'r',
  342. setInterval: 'r',
  343. clearTimeout: 'r',
  344. clearInterval: 'r'
  345. },
  346. },
  347. sandbox: {
  348. evaluate: evaluate,
  349. __exposedProps__: {
  350. evaluate: 'r'
  351. }
  352. },
  353. __exposedProps__: {
  354. timers: 'r',
  355. sandbox: 'r'
  356. }
  357. };
  358. }