WebExtensions/UserScripts
The intent of this document is to clarify the plans for implementing an API which would allow an extension to run third party scripts in isolated sandboxes, and assist in getting it implemented and landed into Firefox.
Some of the reasons for creating this API are:
- Performance: the Greasemonkey-like extensions are currently using inefficient hacks to be able to isolate the userScripts from each other as much as possible.
- Reliability: Greasemonkey-like extensions are currently often using tabs.executeScript API to inject the userScripts, and this implementation strategy is not as reliable as a content script registered to match defined URLs and run at a given phase of the page loading, in particular the injected userScripts may easily miss the "document_start" phase because of race conditions between the page loading and the tabs.executeScript API used to inject the script.
- Security: make easier for an extension to reduce the impact that multiple userScripts may have on each other by executing them in separate sandboxes.
Contents
Overall
The need for this API arises from extensions like Greasemonkey, which allow its user to register/unregister third party userScripts which should be able to access and change webpages that match the userScript options.
The new userScripts API should:
- allow to run each userScript isolated from each other (on the contrary regular content scripts from the same extensions are all executed in the same sandbox).
- not allow the userScripts to directly use any of the WebExtensions APIs available to the "host" extension.
- allow the "host" extension to optionally provide a set of API functions to the userScripts (e.g. Greasemonkey would provide them the "GM_*" functions that are usually available to a Greasemonkey userScript).
The userScript API aims to provide the API needed to allow these userScript to be executed into isolated sandboxes (and the extension to optionally inject its own API methods into these sandboxes) by abstracting (instead of giving the extension direct access to Cu.Sandbox, as it has been requested in "Bug 1353468 - Allow WebExtensions to construct a Cu.Sandbox") the related internal Gecko implementation details.
Timeline
Main tracking bug: 1437098
No results.
0 Total; 0 Open (0%); 0 Resolved (0%); 0 Verified (0%);
This is an ongoing project to provide an API which extensions like Greasemonkey can use to run each of its third party scripts in isolated sandboxes with access to the webpage that their options match.
While this is being built out, developers should understand that this is an experimental API and subject to change.
Milestone 1: Dinamically registered content scripts
In the first milestone we are going to introduce the needed changes to the WebExtensions internals and a new API (contentScripts.register) to allow the WebExtensions content script to be registered programmatically:
- 1332273.
The changes needed to implement the contentScripts.register API are going to be leveraged to implement part of the userScripts API (in particular the userScripts.register API method).
Milestone 2: Dynamically registered user scripts
In this milestone we are going to leverage the work done during milestone 1, which allowed a content script to be registered/unregistered programmatically instead of being declared in the manifest file, to allow a registered script to be executed into an isolated sandbox (instead of a shared sandbox as for the regular extension content scripts).
Besides being able to run a user script into its own isolated sandbox, during this milestone we are going to build the APIs needed to allow the extension to optionally register custom API functions to be provide to the isolated sandboxes (so that the extension can provide its own APIs to the userScripts).
Milestone 3: Improvements
Based on the results of the previous milestones, apply further improvements to the API and/or its implementation.
API
This section provides an idea of the API that the userScripts API namespace may provide to an extension once implemented.
Manifest
Status: not implemented yet (TODO: link to bugzilla issue)
The userScripts API namespace will only be available to the extensions that have a "userScripts" permission:
{ "manifest_version": 2, "name": "example-userscript-manager", "version": "2.0", "background": { "scripts": ["background.js"] }, "permissions": ["userScripts", "<all_urls>", "..."] }
APIs provided in the extension pages
Status: not implemented yet (TODO: link to bugzilla issue))
browser.userScripts.setAPIScript({url: 'apiContentScript.js'}); browser.userScripts.register(userScriptOptions);
These APIs are accessible only in the extension pages (any extension page besides the content script and the content script iframes):
- browser.userScripts.setAPIScript: used to specify an extension Content Script which will be executed (in the content process as a regular content script) to provide the custom APIs to inject into the registered userScripts sandboxes (if executed multiple times, it will replace the API script executed on the next page loads).
- browser.userScripts.register: used to register a userScript and its options (the ones shared with contentScripts.register plus some additional one specific to the userScripts, e.g. scriptMetadata is an opaque object for the WebExtensions API which may contains any metadata that the extension wants to associate to the userScript, as an example Greasemonkey may want to have the userScript name and the array of the granted APIs).
example background.js
// browser.userScripts.setAPIScript: specify an extension content script to execute automatically // when one or more userScripts matches a webpage, see the next section for more details about // how this content script can define which APIs should be available to the userScripts. browser.userScripts.setAPIScript({url: "apiContentScript.js"});
// browser.userScripts.register: register a userScript (and its options). let userScript = await browser.userScripts.register({ source, // [string] Used by the API to know which source to execute in the userScript sandbox // userScripts specific options. scriptMetadata, // [object] A serializable object for use by the extension, opaque to Firefox. sandboxOptions // [object] additional sandbox options we specifically choose to expose, in followups (needs research/sec-reviews/etc) // Options shared with contentScripts.register. matches: [...], excludeMatches: [...], includeGlobs: [...], excludeGlobs: [...], runAt: "...", matchAboutBlank: true/false allFrames: true/false, });
APIs provided in content scripts
Status: not implemented yet (TODO: link to bugzilla issue)
browser.userScripts.injectAPIs({...});
The following APIs are accessible only in a regular WebExtensions content script context (usually from the content script url that has been set in the browser.userScript.setAPIScript, so that the content script will be automatically executed only when there is a userScript that matches a webpage):
- browser.userScripts.injectAPIs: used to register the set of custom API methods to inject in the userScript sandboxes (then the custom API implementation may raise an error when the caller userScript should be able to run that API method, e.g. Greasemonkey may raise an error from an API method that shouldn't be allowed based on the userScript metadata object)
Every custom API method implementation:
- receives two parameters:
- the array of the arguments from the caller
- a userScriptMetadata object (which is the opaque object passed to browser.userScripts.register)
- may use the WebExtensins APIs available to the content scripts, e.g.:
- use the WebExtensions storage APIs (e.g. storage.local/storage.sync, etc.)
- use the WebExtensions messaging APIs to exchange messages with the background pages (e.g. to be able to call WebExtensions API not directly available to the content scripts, like the tabs API)
example apiContentScript.js
// Define an object with all the APIs to inject in the userScript sandboxes. const someCustomUserScriptAPIs = { // someAPICall: name of the API method injected in the sandbox // args: [array] of the arguments of the API call originated from a userScript sandbox // scriptMetadata: [object] the metadata object passed to userScripts.register async someAPICall(args, scriptMetadata) { if (!scriptMetadata.grants.includes("someAPICall")) { // Throws an error (converted by a wrapper implemented internally // into an valid rejection Error instance for the caller sandbox). throw new Error("..."); } const msg = {...} // Use args and scriptMetadata to create a message to send // to the background page. // Send the message to the background page and return the response return await browser.runtime.sendMessage(...); }, async anotherAPICall(args, scriptMetadata) { const data = await browser.storage.local.get(scriptMetadata.name); const res = ... // do something with args and data and return a value. return res; }, ... };
// Set which is the object that contains the custom APIs to inject in the // userScripts sandboxes. browser.userScripts.injectAPIs(someCustomUserScriptAPIs);
Concerns
TBD: security/performance/implementation concerns
- Greasemonkey may need to configure the sandbox differently from how it is usually configured for the regular content scripts, and some additional research/sec-reviews is needed, to determine which kind of sandbox options we can allow and how to expose them (See the following bugzilla comment: https://bugzilla.mozilla.org/show_bug.cgi?id=1353468#c23)
Additional Notes
TBD: additional notes related to the API