Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User scripts in Manifest V3 #279

Open
dotproto opened this issue Sep 20, 2022 · 66 comments
Open

User scripts in Manifest V3 #279

dotproto opened this issue Sep 20, 2022 · 66 comments
Labels
implemented: chrome Implemented in Chrome supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari topic: user scripts

Comments

@dotproto
Copy link
Member

PLACEHOLDER: We have a couple of issues related to user scripts support in Manifest V3, but nothing that currently feels like a good tracking issue. This issue intends to fill that gap by consolidating discussion of Manifest V3 API considerations in one place.

@Rob--W
Copy link
Member

Rob--W commented Sep 21, 2022

@dotproto (on behalf of Google / Chrome) announced the intent to support user script managers in the WECG meeting at TPAC (#232). The meeting notes have been merged in #277 and this topic can be found under "User script manager support in MV3" at https://github.com/w3c/webextensions/blob/main/_minutes/2022-09-15-wecg-tpac.md

I (on behalf of Mozilla / Firefox) previously stated that we'd like to support user script managers, ideally in a more secure way, at https://blog.mozilla.org/addons/2022/05/18/manifest-v3-in-firefox-recap-next-steps/#comment-227473.

EDIT: The API design is ongoing. Significant updates are listed here:

@Rob--W
Copy link
Member

Rob--W commented Sep 23, 2022

To recap from https://github.com/w3c/webextensions/blob/main/_minutes/2022-09-15-wecg-tpac.md, the proposed API is:

  • Accept a code property (with a string) in the individual items of the existing scripting.registerContentScripts API (i.e. theRegisteredContentScript type).
  • Require a (yet to be specified) extension permission to use this feature.
  • Require explicit user consent to allow extensions to use this feature (think of Chrome's "Allow access to local file URLs" option).
  • This basically behaves like tabs.executeScript today, except in a declarative way.
  • The API is primarily designed to account for the use case of user script manager extensions.

My concern with this proposal is that it re-introduces the ability to execute arbitrary code in a (somewhat) privileged execution context, against all efforts to remove this from MV3. In practice, given the absence of anything better, user script managers already execute (third party) user script code in a content script, which shares the extension APIs and some privileges with extensions. This is a suboptimal design.

I propose the following API to support user script managers, which is more secure by design:

  • Support a "code" parameter as documented above.
  • As a restriction, the code can never run in the ISOLATED world (see ExecutionWorld type), only in MAIN and a new USERSCRIPT world (elaborated below). This restriction exists because the ISOLATED world is more privileged than the MAIN world: the isolated world has access to extension APIs and relaxed security policies (e.g. cross-origin requests in Safari, and having a CSP separate from the web page (at least in Chrome and Firefox, I don't know about Safari)).
  • Introduce a new world, "USERSCRIPT" (next to MAIN and ISOLATED), so that extensions still have the ability to run user scripts in an execution environment that's isolated from the web page. When code is set, it will run in this world instead of ISOLATED.
    • At present, I propose only one new world. This is strictly an improvement over the current state of art. Support for more than one world could be useful to extensions, but raises the complexity of implementation, so I'm leaving that for a follow-up and consider it to be out of scope in this proposal.
    • The USERSCRIPT is an isolated world (in the WebKit / Chromium sense) that shares the DOM with the web page. This means that code from both worlds cannot directly interact with each other, except through DOM APIs. When an asymmetric security relationship may exist, the MAIN world is considered to be less privileged than the USERSCRIPT world.
  • User script managers may want to expose somewhat privileged APIs to user scripts (e.g. GM_xmlHttpRequest, GM_getValue, ...). They currently do that by defining these APIs in the content script (ISOLATED) world, supported by the extension APIs available to the ISOLATED world. By forcing user scripts to move from ISOLATED to USERSCRIPT, these extension-defined APIs would at first lose access to the privileged APIs.
    • This access can be restored by establishing a (synchronous) communication channel between the ISOLATED and USERSCRIPT worlds. This can achieved with existing DOM APIs, e.g. with a pre-shared secret (event name) + custom events on shared document/window. This technique may be familiar to some, as it is a way to communicate between MAIN and ISOLATED. Although used in practice, I discourage the use of window.postMessage for communication because that can be intercepted and/or break web pages (for previous discussion, see Proposal: deprecate window.postMessage(message, '*') for use with extensions #78).
    • In the future, a dedicated API to communicate between worlds could be considered.
  • When multiple scripts match and have the same runAt schedule, scripts targeting ISOLATED are executed before USERSCRIPT. This allows the extension to prepare the execution environment before the user script code is executed.
  • Open question: Which CSP to choose for this USERSCRIPT world? There are multiple reasonable options:
    • Same as the main world
    • Same as extension
    • Either of the above two, and customizable in the content_security_policy field in manifest.json and/or scripting API.

Not security-related, but still relevant to consider in the API design:

  • Currently, the RegisteredContentScript type mirrors the input as received by the scripting.registerContentScripts and scripting.updateContentScript methods. The registered items of this type is exposed to the extension via the scripting.getRegisteredContentScripts method. The code parameter could potentially be a large string, so it may make sense to not include the code by default (e.g. by adding a new options object to getRegisteredContentScripts to opt in to receiving the full code)

@xeenon xeenon added the supportive: safari Supportive from Safari label Sep 23, 2022
@jonathanKingston
Copy link

jonathanKingston commented Sep 24, 2022

My concern with this proposal is that it re-introduces the ability to execute arbitrary code in a (somewhat) privileged execution context, against all efforts to remove this from MV3.

I agree with this concern.

chrome.scripting.globalParams was added to solve similar use-cases however it's not ever exposed to the main world scripts. I'd instead propose a way to allow these variables to be passed into the script (perhaps cloned so that modification isn't possible) some avenues to inject these would be:

  1. document.currentScript.params
  2. import.meta.params
  3. Inject in a script local variable prior to execution of the script. (I believe all JS engines have the ability to easily register script local properties programatically)

I'd not expose these on globalThis/window as ideally the page would have no visibility of these parameters.

@derjanb
Copy link

derjanb commented Sep 29, 2022

@dotproto @Rob--W thanks for addressing this now.

Require explicit user consent to allow extensions to use this feature

Will this be automatically set to on for all existing Manifest v2 installations on update to the mv3 version?

Introduce a new world, "USERSCRIPT"

This sounds promising and will make securing a lot of functions and prototypes that can be altered by the page obsolete.

The USERSCRIPT is an isolated world (in the WebKit / Chromium sense) that shares the DOM with the web page. This means that code from both worlds cannot directly interact with each other, except through DOM APIs.

This is a problem. I'd say more than the half of the userscripts directly interact with the page's JavaScript objects and functions.

Tampermonkey at Firefox (BETA for now - the stable release will follow soon) for example uses Firefox's userScripts API which creates a context separated from the page (and the extension), but still allows access to the main world (unsafeWindow).

When multiple scripts match and have the same runAt schedule, scripts targeting ISOLATED are executed before USERSCRIPT.

👍

Open question: Which CSP to choose for this USERSCRIPT world?

Userscripts should not be limited by a "foreign" CSP (extension or main world) . They are made to do things that neither Tampermonkey nor the page expect. Ideally we could add a csp property additionally to the code property and Tampermonkey could either introduce a @csp userscript header that is forwarded to scripting.registerContentScripts or use existing tags to create a CSP specific to this userscript.

@Sxderp
Copy link

Sxderp commented Sep 30, 2022

I second @derjanb concern with limiting interaction via DOM APIs. This limits what can be done in terms of userscripts. Being able to mess with the webpage JS is a key point.

I also liked how Firefox implemented the userScript API. A separate context was created but you could inject additional functions into it via a callback. If the communications between USERSCRIPT and ISOLATED is also going to be limited to DOM APIs isn't that also risky? Under this, I'm assuming that extension specific APIs are not available. How can a pre-shared secret between the userscript manager and userscript be used? The secret would need to be shared to all userscript authors? Maybe I'm misunderstanding but the more I look at this the more I think "can't webpages hi-jack this?"

@Illya9999
Copy link

Illya9999 commented Oct 21, 2022

@Sxderp

can't webpages hi-jack this?

Yes. Webpages can run code before userscript managers would ever be able to by using iframes or popup windows. It has been used maliciously in the past and is now partially fixed in some userscript managers, however there is no complete 100% fix. The issue of webpages being able to execute code before extensions was reported to chromium some time ago and was marked as wontfix iirc

@muzuiget
Copy link

Currently we can execute arbitrary code in MAIN world on Chrome 107 with these code:

// background.js
chrome.scripting.registerContentScripts([
    {
        id: '0',
        matches: [
            'https://www.example.com/*',
        ],
        js: [
            'main-world-script.js',
        ],
        world: 'MAIN',
        runAt: 'document_idle',
    },
]);
// main-world-script.js
window.addEventListener('message', (event) => {
    // message from ISOLATED world content scripts
    const message = event.data;
    if (message.name === 'user-script') {
        const script = document.createElement('script');
        script.textContent = message.text;
        document.head.appendChild(script);
        script.remove();
        return;
    }
});

Most user scripts do not need to as complex as Greasemonkey-like scirpts:

  • No GM_* and chrome.* privileged functions.
  • No automatically update from remote url.
  • Execute on document_idle, the DOM API is accessable.

Just like users execute scripts in the DevTools "Console" or "Source -> Sinppet", but use extensions to execute scripts automatically base on the page url.

We can create a lite version user script manager with Manifest V3 now.

But does the Chrome Store policy allow this usage?

@EmiliaPaz
Copy link
Contributor

Hi everyone!
My name is Emilia Paz and I am part of the Chrome Extensions team. I'll be working on providing user script support in the Scripting API for MV3, as discussed in this thread.

I started an API proposal doc that takes into account the points discussed here and in offline conversations. The goal is to align on an API design that works better for everyone. Please, feel free to comment on the doc or in this issue. Also, I'll be attending the next WECG meetings for further discussion.

Looking forward to working together

@BlackGlory
Copy link

BlackGlory commented Nov 4, 2022

Does the current proposal imply that a user script will only run on a page?
What if a user script needs to run in a sandbox unrelated to a page?

@tophf
Copy link

tophf commented Nov 4, 2022

I started an API proposal doc

Methods that contain Content are already misleading because they also deal with main-world scripts that aren't content scripts by definition, but page scripts: #313. If you want to separate methods by areas of concern then there should be Page methods e.g. registerPageScripts and so on, thus obviating the need for world parameter in Content and Page methods.

@tophf
Copy link

tophf commented Nov 4, 2022

The main problem is that this API proposal is currently unusable in Violentmonkey or Tampermonkey because it's completely insecure due to https://crbug.com/1261964, to circumvent which these extensions currently have to extract the safe methods from a temporary iframe before running the main world scripts. It means that the main world script cannot call any standard method like addEventListener until the extracted methods are passed securely from this iframe in a manner that cannot be intercepted/spoofed by another main-world script.

Unless this bug is fixed or a secure communication method is provided, these extensions will become wide-open backdoors and I will suggest their authors to remove them from the web store. The method must be synchronous, by the way.

@tophf
Copy link

tophf commented Nov 4, 2022

Another nasty problem is that the API proposal doesn't support @include and @exclude which are glob-based by default but can be RegExp as well, which is used by tons of userscripts and there's no good alternative for RegExp. With the current API shape any userscript with a RegExp include/exclude that runs at document_start will have to run on all sites and then the extension's content script will perform the check, which is extremely wasteful (due to the need to inject the script into every web page + the lack of code compilation cache for it) and can take a long time depending on the regexp.

@brunoais
Copy link

brunoais commented Nov 4, 2022

@tophf: I made some suggestion changes to the document. They haven't been approved but I think it's worthwhile for you to take a look at them and check if they suffice.

@EmiliaPaz
Copy link
Contributor

EmiliaPaz commented Nov 4, 2022

Does the current proposal imply that a user script will only run on a page? What if a user script needs to run in a sandbox unrelated to a page?

User scripts will only be allowed to execute in the MAIN world (page) or a new USER_SCRIPT world.

@Sxderp
Copy link

Sxderp commented Nov 4, 2022

Is there some way to provide additional functionality (GM.xhr and friends) to a user script? For example the Firefox userScripts.onBeforeScript let you run a callback to export functions into the execution environment. The current proposal has no such thing and thus the best alternative is to concatenate some fixed string of functions to the "code" argument?

@brunoais
Copy link

brunoais commented Nov 4, 2022

Is there some way to provide additional functionality (GM.xhr and friends) to a user script?

@Sxderp It's there. Just not much fanfare calling to it:

scripts targeting ISOLATED are executed before USERSCRIPT. This allows the extension to prepare the execution environment before the user script code is executed.

This is inside "Other considerations" sub-chapter

@hanguokai
Copy link
Member

I'll be attending the next WECG meetings for further discussion.

I would suggest holding a separate topic-specific meeting rather than discussing it in a regular meeting. Browser vendors and scripts manager maintainers are invited to participate. (PS: I am not script manager author)

Having different types and methods than content scripts is beneficial in the following ways: ……

I recommend using a completely different namespace(e.g. chrome.userscripts.register()) so that they are in completely different document pages and developers don't get confused.

My other thoughts

What I understand is this proposal still allows arbitrary local and remote code to be executed in the main or userscript world of web pages, just don't allow to use extension api.

Besides extension and theme, can the web store support a third type of user script? All user scripts are submitted to the store and reviewed? Thus, user scripts are trustworthy and deterministic like extensions.

Compared to user scripts, I think content scripts should also allow arbitrary code execution. Because, when websites changed their web pages's structure/code/style, content scripts also need to be updated quickly to adapt to changes.(Usually, extenson developers do not own the site)

@WaqasIbrahim

This comment was marked as off-topic.

@7nik
Copy link

7nik commented Nov 8, 2022

What I understand is this proposal still allows arbitrary local and remote code to be executed in the main or userscript world of web pages, just don't allow to use extension API.

The extension needs additional permission for it. Thus the market can warn users that it's a potentially dangerous extension, also, reviewers will check if it's a valid usage of usercripts, like e.g. a weather app doesn't need it and it will very suspicious if it asks for it.

Besides extension and theme, can the web store support a third type of user script?

No. Sometimes you need US only for yourself, and sometimes even for a short period of time. Also, if a US need only a very limited number of people (<10), there is no reason to "publish" it.
Also, there are in times, in dozens more US than extensions, and Google won't be glad to spend money on reviewing every single US.

Compared to user scripts, I think content scripts should also allow arbitrary code execution.

Enforcing security is one of the main goals in the MV3, and forbidding arbitrary code execution is one of the main ways to achieve it.

Because, when websites changed their web pages's structure/code/style, content scripts also need to be updated quickly to adapt to changes.

Sites do not roll up changes every single day, most of them even prefer to avoid this (it requires spending money and receiving usually angry feedback that things anymore aren't what the user used to, unless it's bug fixing). Also, you can in an empirical way find general and fuzzy approaches that are more or less resistant to minor changes in the page layout.

@hanguokai
Copy link
Member

No. Sometimes you need US only for yourself, and sometimes even for a short period of time. Also, if a US need only a very limited number of people (<10), there is no reason to "publish" it.

I understand. In my previous comment, I said "script manager can supports run both trusted and untrusted code, but gives different user warnings.". In other words, no matter it is published to the store or not, users can run them.

Also, there are in times, in dozens more US than extensions, and Google won't be glad to spend money on reviewing every single US.

This question is best answered by the extension stores. I don't know what they think.

Enforcing security is one of the main goals in the MV3, and forbidding arbitrary code execution is one of the main ways to achieve it.

In this way, I think user scripts should be subject to the same restrictions as content scripts.

Sites do not roll up changes every single day, most of them even prefer to avoid this……

I often see extension developers complaining that a website update caused its code to go wrong, and they need to update the extension immediately.

@EmiliaPaz
Copy link
Contributor

@tophf

Methods that contain Content are already misleading because they also deal with main-world scripts that aren't content scripts by definition, but page scripts: #313. If you want to separate methods by areas of concern then there should be Page methods e.g. registerPageScripts and so on, thus obviating the need for world parameter in Content and Page methods.

First off, chrome.scripting.getRegisteredContentScripts should have been chrome.scripting.getRegisteredUserScripts. My bad, fixed it.

As for the separate methods, correct me if I am wrong, you are suggesting having separate methods for user scripts, content scripts and page scripts (basically dividing content and page scripts) or either changing the current content script methods (issue #313). Content and page scripts behave similarly, have the same permissions enforcement and most importantly they don't support remote host code. Thus they can be grouped under the same methods (as they are currently) with different worlds. User scripts have to be clearly differentiated for better documentation and stronger enforcement of remote host code abuse. Thus the introduction of new methods. However, as @hanguokai points out, maybe is better having a new namespace.

As for your rename suggestion, I see the issue is listed as an Agenda discussion for public meeting on 2022-11-10 so better to follow up there.

The main problem is that this API proposal is currently unusable in Violentmonkey or Tampermonkey because it's completely insecure due to https://crbug.com/1261964, to circumvent which these extensions currently have to extract the safe methods from a temporary iframe before running the main world scripts. It means that the main world script cannot call any standard method like addEventListener until the extracted methods are passed securely from this iframe in a manner that cannot be intercepted/spoofed by another main-world script.

Unless this bug is fixed or a secure communication method is provided, these extensions will become wide-open backdoors and I will suggest their authors to remove them from the web store. The method must be synchronous, by the way.

I was not aware of this issue. I'll contact the OWNER and see what's the status on this. Thanks for pointing it out

Another nasty problem is that the API proposal doesn't support @include and @exclude which are glob-based by default but can be RegExp as well, which is used by tons of userscripts and there's no good alternative for RegExp. With the current API shape any userscript with a RegExp include/exclude that runs at document_start will have to run on all sites and then the extension's content script will perform the check, which is extremely wasteful (due to the need to inject the script into every web page + the lack of code compilation cache for it) and can take a long time depending on the regexp.

How are RegExp currently supported in MV2? I believe chrome.tabs.executeScript doesn't support @include and @exclude. If they are not currently supported, they may not be included on the API v1 since we are trying to achieve functional parity with MV2. However, do you have any suggestions on how this can be achieved?

Sorry for the late response everyone. I am getting myself familiar with user scripts and user scripts managers to provide better answers. Will continue shortly.

@tophf
Copy link

tophf commented Nov 8, 2022

Content and page scripts behave similarly, have the same permissions enforcement and most importantly they don't support remote host code. Thus they can be grouped under the same methods (as they are currently) with different worlds

As I pointed out in #313 they are so different that you could say they're "worlds apart", pun intended, so conflating them is a very dangerous path that misleads developers into believing their data and code are safe from interception/spoofing/poisoning.

How are RegExp currently supported in MV2?

There's no support at all. The closest thing is RE2 syntax in filtered events of declarativeContent or webNavigation API.

Currently userscript managers explicitly use RegExp matching in JS. It doesn't cost much in ManifestV2 because the regexps are compiled just once in the persistent background script. Doing the same in a ManifestV3 service worker will be very costly due to the need to recompile regexps every time the SW starts for a new URL navigation, and it won't guarantee document_start timing.

I understand that Chrome won't add full RegExp to its API, so the next best thing is implementing RE2-compatible matchesRegex and excludeMatchesRegex and a method to test compatibility similar to chrome.declarativeNetRequest.isRegexSupported, so that userscript managers can simplify the userscript's regex for the API and then narrow it down inside the content script using the full RegExp check, thus reducing the wasteful injections.

@Rob--W
Copy link
Member

Rob--W commented Nov 10, 2022

@EmiliaPaz Thanks for sharing your proposal! I'll share my feedback here, because it's difficult to trace or reference discussions on Google docs, especially on a document that's still undergoing revisions.

Your stated goal of the document is "Achieve functional parity with MV2 for user script managers.". I note that just MV2 parity is a low bar. Due to the lack of supporting APIs from the browser, user script managers are built insecurely. Improving that would be great.

I'll describe the needs of user script managers in more detail, particularly focused on security and feasibility. After that I'll summarize the requirements as a starting point for the API design. I am intentionally focusing on parts that cannot be implemented by user script managers themselves, as these should be the top priority. E.g. regex matching can be implemented by user script manager code themselves, and support for that could be a future enhancement.

Background

There are many user script managers (USM hereafter), with the most popular ones being Greasemonkey, Tampermonkey and Violentmonkey. This section describes how they work, and the core primitives that they rely on today. Other than the "userscript" world, all concepts mentioned here already exist in MV2 today.

User scripts run on top of existing web pages, and need to access the DOM and/or JS variables in the page's context and/or semi-privileged user script APIs:

The following questions are relevant for the choice of world for a user script:

  • Does the user script need access to variables in the web page? (e.g. via unsafeWindow).
  • Does the user script need access to semi-privileged user script APIs (e.g. GM)?
need page access need GM access Ideally, the world to run in World used by MV2 USM
no no User script world Main world or isolated world (of content script)
no yes User script world, with prepared environment Main world or isolated world (of content script)
yes no Main world Main world
yes yes Main world (or user script world + supporting code in main world) Main world

When semi-privileged APIs are not needed, a USM only needs the ability to run some given string as JavaScript code. Ideally in a world distinct from the page/content script, i.e. the "userscript" world proposed in #279 (comment).
When semi-privileged APIs are needed, but page access is not, likewise (with some extra work from the USM to expose the APIs).

⚠️ When both accesses are needed, the current APIs are insufficient. This is due to the fact that the execution environments of web pages are untrusted. Untrusted web pages can intercept and change the behavior of all global and built-in methods, which makes it near-impossible to securely expose semi-privileged APIs to user scripts in the main world.

⚠️ USM currently try to work around the restrictions by running code in very early in the main world (at document_start), that saves all necessary (DOM/JS) API references in local variables, under the assumption that the web page has not had a chance to modify the execution environment. This has limited success, because the assumption is not always valid (e.g. https://crbug.com/1261964 mentioned by #279 (comment)).

This is generally the structure of any script that introduces semi-privileged functionality to the main world. It itself runs in the main world (the concept is also relevant outside user scripts):

(function(secrets) {
  // Step 1: Save a reference to all standard DOM API methods, to avoid tampering by the web page.
  // Step 2: Use |secrets| and DOM APIs to create a semi-secure communication channel, e.g. using `CustomEvent`.
  // Step 3: Via this semi-secure channel, retrieve necessary data such as user script metadata.
  // Step 4: For *each* user script, create the GM API interface and execute the user script:
  somehowExecuteUserScript(localVarWithGMAPI, window, etc);
})("secret communication ID here");

⚠️ The snippet above requires a random secret variable, available only to the two ends of the communication channel: the above code in the main world, and the other side in the (privileged) content script.

Ordinarily, web pages and also scripts in the main world can use window.eval to execute code. This can however not be relied upon because it may be blocked by the page's CSP (script-src without 'unsafe-eval'). The actual content script code is therefore executed by inserting an inline <script> element containing the definition of a user script (this is blocked in MV3 due to the CSP for content scripts that blocks remote code):

window.someSecretRandomName = function(GM, unsafeWindow, etc) {
  /* actual user script code here */
};

The user script is wrapped in a function, because it needs a closure with a reference to the semi-privileged API, which it receives when called by the previous script that starts with (function(secrets) {.
⚠️ Because of the reliance on the global, web pages can intercept read/writes to that value, and therefore get a reference to the semi-privileged API exposed to it. Or read the source code of the user script.

Identified requirements

The following required primitives can be derived from the above overview of how user script managers work:

  • A mechanism to execute code (user script or USM API definition) in the main world.
  • The ability to execute code in a world different from the main world (current practice is the "isolated" world, but ideally the "userscript" world proposed in User scripts in Manifest V3 #279 (comment)).
  • A mechanism for that code to receive a set of private well-known safe values/API references that cannot be tampered with by the execution environment of the main world (resilient against prototype pollution).
  • A mechanism for the user script manager to selectively expose APIs to user scripts, potentially distinct for each user script.

@EmiliaPaz 's proposal in #279 (comment) covers the first two aspects, but not the other two.

There are many different ways to implement these requirements. I will show two examples, one userscript-specific API (from Firefox) and a more generic API that has broader applicability.

Final note: the above requirements are minimal. If the goal is to move the responsibility of matching and triggering script execution from the extension to the browser, then there should also be a way to register some metadata for a script that is synchronously available. It would also be nice if there is a way for content script to veto the execution of a user script. These can be supported in the two API designs below.

Firefox's userScripts API (MV2)

Firefox's userScripts API was mentioned before in #279 (comment). I'll briefly describe the relevant parts of the API design for inspiration.

Features of the userScripts API:

  • Each user script is executed in its own isolated execution environment.
  • userScripts.register method to register user script code, including the ability to define metadata for the api_script.
  • Optional user_scripts.api_script manifest key to specify a script that is expected to prepare the execution environment of the user script.
  • api_script's userScripts.onBeforeScript event to notify the api_script before a user script is executed, to prepare the execution context of the environment. It receives the metadata from the script registration, to allow the USM to selectively expose APIs via the defineGlobals method.

In Firefox, it is possible to have asymmetric access relationships between code from different execution environments (Xray Vision). This mechanism enables user script managers in Firefox to prepare the environment beyond what's possible with defineGlobals, by assigning values directly to the execution context of the user script. User scripts cannot read/write values in the opposite direction; user script managers would have to use the export API passed to the onBeforeScript method to export the value to the user script.

The api_script from this API is created once per process as an optimization. It is not strictly necessary, so if desired this characteristic can be dropped. The onBeforeScript event can be added to a content script instead.

Note: we have marked the userScripts API as MV2-only in Firefox because the script registration mechanism is incompatible with the lifetime characteristics of background scripts in MV3. Furthermore, we would like to establish a cross-browser-compatible API for user script managers.

Scripting API with cross-world communication

To avoid confusion, this example extends the current scripting API, not the updateUserScripts API sketched in @EmiliaPaz's proposal.

  • The scripting.registerContentScripts method receives two new options:
    • code - code to run in the main or isolated world
    • extraArgs - list of parameters that should be resolved by the content script. This cannot be used if the world is "ISOLATED". Note that there is already an args option in the API; that could be re-used to support static metadata.
  • chrome.scriptingContent.onRequestArg - a new event in the content script (isolated world) to allow the content script to respond to requests for arguments.
    • I named this "Content" because this API namespace is part of scripts running in the (web) content process, opposed to the extension process.
// Somewhere in the extension, e.g. extension page where the user edits the user script.
chrome.scripting.registerUserScripts({
  code: `
    // This is what the user script manager generated:
    let GM = {
      info: GM_info,
    };
    let unsafeWindow = window; // In the main world, unsafeWindow is the window.
    let someRandomNumber = args[0];
    args = undefined;
    // This is the actual user script:
    console.log(hello, GM.info(), unsafeWindow);
    // ^ note: console is a global variable. The web page could have tampered with it...
  `,
  externalArgs: ["hello", "GM_info"],
  args: [4], // <-- someRandomNumber
  id: "my-userscript",
  world: "MAIN",
  matches: ["https://example.com/*"],
});

Internally, the browser could convert the above to the following:

function(args, hello, GM_info) {
/* the code string here */
}  // TODO: browser should find the hello & GM_info args and call this function

Then, before the browser runs the user script, it asks the content script for the API definition. For example:

chrome.scriptingContent.onArgRequest.addListener(arg => {
  if (arg === "hello") {
    // We must at least support primitive values (string, number, etc).
    return "hello world";
  }
  if (arg === "GM_info") {
    // We must also support functions. Potentially async (i.e. returning a Promise).
    return async () => {
      // The functions themselves may return values. TODO: define acceptable return values.
      return { plain: "object", is: "safe", but: "what", about: Function ? "callable properties" : "?" };
    };
  }
});

To maximize utility, I suggest to support at least the following return types:

  • Functions. The functions themselves should return a value that matches this format.
  • DOM elements, anything for which a meaningful DOM wrapper exists across worlds.
  • Anything else that can be structurally cloned.

The above description consists of standard types that are well-supported across browsers. Objects with getters, setters or function properties are not supported.

Note: the above is intentionally generic to cover broader use cases such as #284.

@EmiliaPaz
Copy link
Contributor

@Sxderp

Is there some way to provide additional functionality (GM.xhr and friends) to a user script? For example the Firefox userScripts.onBeforeScript let you run a callback to export functions into the execution environment. The current proposal has no such thing and thus the best alternative is to concatenate some fixed string of functions to the "code" argument?

Is XHR necessary or can it be shimmed using fetch? Key constraint is XHR supports blocking requests.

@EmiliaPaz
Copy link
Contributor

EmiliaPaz commented Nov 10, 2022

Another nasty problem is that the API proposal doesn't support @include and @exclude which are glob-based by default but can be RegExp as well, which is used by tons of userscripts and there's no good alternative for RegExp. With the current API shape any userscript with a RegExp include/exclude that runs at document_start will have to run on all sites and then the extension's content script will perform the check, which is extremely wasteful (due to the need to inject the script into every web page + the lack of code compilation cache for it) and can take a long time depending on the regexp.

Do you, or someone else, know how close do we get with:

  • Just matches (excludeMatches)
  • matches + globs
  • matches + regex
  • matches, globs, and regex
    Basically wondering how much we lose without regex support, and if supporting globs would make a meaningful dent.

EDIT: I just realized @Rob--W commented, and included regex. Will go over it and comment back

@Robbendebiene
Copy link

Robbendebiene commented Jan 3, 2023

Just want to point out, that there are other use cases for injecting user scripts than just by match patterns.
For example mouse gesture extensions want to inject them into a certain tab (active tab) at a certain point (at the end of a a gesture).

Example add-ons with code/implementation reference:

Therefore I plead to have a code parameter for the scripting.executeScript() function as well.

Rob--W added a commit that referenced this issue Feb 16, 2023
This proposal is part of #279

Co-authored-by: Emilia Paz <[email protected]>
Co-authored-by: Simeon Vincent <[email protected]>
Co-authored-by: Rob Wu <[email protected]>
@rdcronin
Copy link
Contributor

This is implemented in Chrome. Future expansions will be tracked separately (you can see some of these in @Rob--W 's comment at #279 (comment)). We'll leave this issue open until other browsers implement the core API, and then close this out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
implemented: chrome Implemented in Chrome supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari topic: user scripts
Projects
None yet
Development

No branches or pull requests