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

Add User Scripts API proposal #331

Merged
merged 18 commits into from
Feb 16, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions proposals/user-scripts-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# User Scripts API

## Background

### User Scripts and User Scripts Managers

[User scripts](https://en.wikipedia.org/wiki/Userscript) are (usually relatively small) snippets of code that are injected into web pages in order to modify the page's appearance or behavior. User script managers are a type of extension that is used to manage the collection of these user scripts, determining when and how to inject them on web pages.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the API design, it's relevant to consider that there are scripts requiring access to JS in the page's context and/or whether they need access to the user script API. As seen in the table at "Background" at #279 (comment)

The world + code part of this proposal fully covers the first two rows.

The (existing) scripting API plus its (existing) main world covers the third row.

The fourth row (i.e. page access + user script API access) is being addressed by the proposed CSP relaxation here. That chosen approach removes the ability for the browser to take control of the injection though (but this is already an issue in MV2, so that could be iterated on later).


User scripts can be created directly by the user or found in a number of different user script repositories around the web.

### Manifest V3

Manifest Version 3 (MV3) restricts the ability for extensions to use remotely-hosted code since it is a major security risk (cannot review or audit code that isn't in the extension package). This is directly at odds with user script managers, which fundamentally require the execution of arbitrary code (the user scripts themselves). This creates a feature gap between MV2 and MV3.

## Goal

User scripts satisfy an important use case for power users, allowing them to quickly and easily tweak the functionality of a web page. Therefore, we propose adding a **new API to register user scripts with arbitrary code** targeted specifically for user scripts. However, we will also take a number of steps to help reduce the abuse-ability of this API and ensure we don't lose the benefits of the remotely-hosted code restrictions from MV3.

### Multi-phase design

User script support will have a multiphase design and implementation process. The initial implementation has the following goals:

- Achieving functional parity with MV2 for user scripts managers, enabling migration to Manifest V3
- Setting the foundations needed for future enhancements that will allow the browser to take more responsibility for the user script injection
- Limiting abusability

### Initial Requirements

The rest of this proposal focuses on the first iteration with the following requirements:

- **(A)** A mechanism to execute code in the main world
- **(B)** The ability to execute code (with a separate CSP) in a world different from the main world and the extension's isolated world
- **(C)** A separate user script permission
- **(D)** Communication between JavaScript worlds
EmiliaPaz marked this conversation as resolved.
Show resolved Hide resolved

## Proposal

### New Namespace

User scripting related features will be exposed in a new API namespace, tentatively named `userScripts`. The proposal authors favor the use of a new namespace for several reasons.

1. **Better set developer expectations.** The clear separation between user and content scripts will reduce confusion for developers for which API methods to use and what the capabilities / restrictions of each are. This naming also more clearly communicates that this capability is not meant as a general purpose way to inject arbitrary scripts.

2. **Stronger enforcement of remotely hosted code abuse.** A distinct namespace allows people to more clearly see where user scripts features are being used and more easily spot abuse patterns.

3. **Easier engineering implementation.** Browser vendors should be able to restrict the execution world and different API methods behind features.

4. **Introduce a user script world with custom CSP.** Allow user scripts to opt-in to a more secure world that is only available in this namespace.

### API Schema

#### Types

```
dictionary RegisteredUserScript {
boolean? allFrames;
ScriptSource[] js;
xeenon marked this conversation as resolved.
Show resolved Hide resolved
string[]? excludeMatches;
string id;
string[]? matches;
// Default to `document_idle`
RunAt runAt;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many userscripts need document_body which means the moment the <body> element appears. If this mode is not implemented in Chrome natively, each USM will have to re-implement it via a workaround that may behave differently.

// Allows `USER_SCRIPT` (default) and `MAIN`
// and returns error for `ISOLATED`.
ExecutionWorld? world;
// Implemented as disjunction: runs in documents whose URL matches
// "matches" or "includeGlobs", and not "excludeMatches" nor "excludeGlobs".
string[]? includeGlobs;
EmiliaPaz marked this conversation as resolved.
Show resolved Hide resolved
string[]? excludeGlobs;
}
dictionary UserScriptFilter {
string[]? ids;
}
// Must specify exactly one of: `file` or `code`.
dictionary ScriptSource {
string? code;
string? file;
}
```

#### Methods
When `callback` is omitted from these methods, a `Promise` is returned instead of `undefined`.

EmiliaPaz marked this conversation as resolved.
Show resolved Hide resolved
```
browser.userScripts.register(
scripts: RegisteredUserScript[],
callback?: function
)
browser.userScripts.unregister(
filter?: UserScriptFilter[],
callback?: function
)
browser.userScripts.getScripts(
filter?: UserScriptFilter[],
callback?: function // called with (RegisteredUserScript[]).
)
browser.userScripts.update(
scripts: RegisteredUserScript[],
callback?: function
)
```

### Requirements

#### A. A mechanism to execute code in the main world

- User scripts can be registered in the `MAIN` world.
- Extension can customize the `USER_SCRIPT` world's CSP to inject a script tag into the host page (expanded in requirement B)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this intended mechanism of unblocking user script managers in the transition to MV3, the minimum necessary to support user script managers is just to allow then to insert inline script elements in the web page, from the content script (not necessarily a user script world).

The current proposal proposes a runtime method to override the CSP of the user script world. The dynamic aspect of the runtime API is not required, a static manifest key would be enough for the use case. This feature existed before in the form of the content_security_policy.isolated_world manifest key but was dropped in https://chromium.googlesource.com/chromium/src.git/+/345390adf6505881f84da2351c3e4fc1b06dac26%5E%21/ after concerns over the ability to enforce it.

To unblock the user script managers from transitioning from MV2 to MV3, it would suffice to allow the CSP for content scripts to be customized, to relax the current MV3 CSP requirements by accepting 'unsafe-inline' too. With this added relaxation, the least secure CSP that the extension can have is script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline'. Because this still excludes unsafe-eval, extensions cannot have arbitrary RCE in the (privileged) content script world, while still being able to run inline code in the main world.

The approach sketched in this comment meets requirements A, B and D (D because the extension can try to carefully sets up its own channel, which it can because it controls the script injection). Requirement C is independent and optional: because the feature is toggled through the manifest.json file, its presence can be statically detected by the browser and automated scanners and/or reviewers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am reading correctly, there are two proposals here (and in the next comment):

  1. Relax the current MV3 CSP requirements in the extension ISOLATED world by accepting 'unsafe-inline' too, or
  2. Static user script world customization in the manifest

For 1.,’unsafe_inline’ allows remote host code. Thus, we cannot allow it in the extension ISOLATED world as it
would affect all content scripts and not just user scripts.

For 2., static world customization is easier, since most extensions will set the user script manifest, and makes it easier for revision. However, dynamic world customization allows to dynamically change CSP. This can be useful for user script managers as they can set the CSP based on the user scripts. Additionally, in the future we would like to have separate USER_SCRIPT worlds. Having manifest entries for each world would be messy, thus a method is preferred.
Thus, dynamic CSP customization is better for user script managers overall and allows for extensibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments, Rob and Emilia!

@Rob--W : As Emilia points out, the main reason for not just allowing unsafe-inline in an isolated world CSP is that we want to gate this behavior on the user script permission. Additionally, only allowing it in the user script world (rather than in the main isolated world) limits the permission of that code, which is also desirable.

Because this still excludes unsafe-eval, extensions cannot have arbitrary RCE in the (privileged) content script world, while still being able to run inline code in the main world.

I disagree with this. unsafe-inline is sufficient to have full remote code execution, AFAIK. Consider:

(async () => {
  let fetchResult = await fetch(...);
  let evilCode = await fetchResult.text();
  let scriptTag = document.createElement('script');
  let scriptTag.text = evilCode;
  document.body.appendChild(scriptTag);
})();

This will execute any arbitrarily-fetched code and requires only unsafe-inline (not unsafe-eval). Because of this, we don't want to allow arbitrary content scripts to have this relaxed CSP.

As you've correctly highlighted, our first goal here is to allow user script managers to migrate to MV3. We'd like to provide them with more capabilities in the future (including some form of secure inter-world communication) and allow the browser to take on more of the injection duties; this is something we'll continue iterating on. But we'd like this initial version to unblock the transition such that user script managers can behave similarly to how they do in MV2.

To clarify: If we provide the capabilities outlined in this proposal (the ability to inject arbitrary code into a less-permissioned user script world that has a relaxed CSP), do you agree user script managers should be able to migrate?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may also be relevant to consider how many userscripts would cease to function without certain capabilities, even if the bare functioning of the userscript manager itself is migrated.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: If we provide the capabilities outlined in this proposal (the ability to inject arbitrary code into a less-permissioned user script world that has a relaxed CSP), do you agree user script managers should be able to migrate?

@rdcronin It depends.

Being able to execute arbitrary code in a userscript world is one step and some scripts would already work then, but many userscripts need access to unsafeWindow which is the main world window object.

So either the userscript world can access the main world window object like the contentWindow of an iframe or "has a relaxed CSP" means the userscript world is able to inject arbitrary code into the page and this code is exempt from the page's CSP (like script tags injected by a content script in mv2).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rdcronin

Would this allow us to eliminate the need for three-party (ISOLATED, USER_SCRIPT, and MAIN worlds) communication entirely?
This wouldn't allow for synchronous access to any APIs available in content scripts. [...] Are any of these critical for user script functionality?

None of the mentioned ones is critical, but as @tophf mentioned a replacement for runtime.sendMessage and if possible runtime.connect is required.

I'm however not sure that empowering the USER_SCRIPT world to become a "ISOLATED world" light is the right way.
If you ask me (and it's also part of @tophf's preferably list), then userscripts should be executed in the USER_SCRIPT world some day and be able to access the MAIN world via a iframe contentWindow-like object.
This way userscripts would have a clean environment and prototypes. And if the USM can control the CSP of the USER_SCRIPT world, it can use it now to emulate MV2 behavior (run in MAIN_WORLD) and once the MAIN_WORLD "contentWindow" is available run them in the USER_SCRIPT world.

I see that this is only called out as "limited support" on the tampermonkey docs?

Content script (and the initial main world script) injection needs to be at document_start.
The docs mention limited support for document_start userscript injection because the scripts can not be transferred synchronously to the content script (except if using the cookie+objectURL hack for critical scripts). Therefore userscripts might run later than document_start.

We'd never be able to guarantee that the main world couldn't interact or affect the injected script.

I know. That's why we are working on a future path to improve this. 😉 This is also another chance to advertise my MAIN_WORLD contentWindow. 😁

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this seems promising : )

I think this is worth pursuing as a way to unblock MV3 migration for USMs, and once this initial phase is agreed upon, we can begin iterating on enhancements to make it better and move more towards a future where the browser can take on more of the injection responsibilities.

Concretely, how about we update this PR to include:

  1. The ability to register/unregister/configure user script objects (userScripts.register(), userScripts.unregister(), userScripts.getScripts(), userScripts.update()). This is largely unchanged from the current draft (though perhaps we should add a file parameter to the registered script, to reflect how it is more likely to be used at first).
  2. The ability to relax CSP in the user script world and control whether any extension APIs are exposed. I suggest we handle both of these with the configureWorld() function by changing the argument to be an object like { csp?: string, enableMessaging?: bool }. If csp is undefined, the isolated world CSP is used; if enableMessaging is undefined or false, no extension APIs are exposed. if csp is defined, that CSP is used in the USER_SCRIPT world. If enableMessaging is true, we expose chrome.runtime.sendMessage() and chrome.runtime.connect(). Here, I'm suggesting the runtime variants here, instead of something like userScripts.sendMessage, to align more with the existing pattern used by both web pages and content scripts; I'm open to bikeshedding this. I suggest enableMessaging (and not enableAPIs) to account for more flexibility in the future, e.g. whether the extension may want to control if the dom API is exposed.
  3. Introducing a new event for receiving messages from user scripts (similar to the prior art of runtime.onMessageExternal). userScripts.onMessage and userScripts.onConnect make sense to me, but I could see an argument for following the pattern of runtime here, too, and introducing a runtime.onUserScriptMessage() and runtime.onUserScriptConnect.

With these changes, USMs will be able to migrate to MV3, but will have to continue jumping through the same hoops they do today. It would be possible that a few user script injections could be delegated to the browser by registering them with world: main if they were ones that didn't require any communication with the extension; I don't know how easily that can be determined from the user script metadata.

Subsequently, file new issues (or reuse existing ones) for some of the items we've discussed for future improvements:

  1. Allow user scripts to inject in unique worlds. I'd propose we do this by the introduction of a worldId in userScripts.register(); user scripts with the same worldId inject in the same world. If undefined, user scripts are injected into the default user script world (say, ID 0). configureWorld can then be expanded with a worldId to allow configuring individual worlds' properties (both CSP and availability of APIs). I think this is a relatively straigthforward and desirable change, with the open questions of performance.
  2. Providing an explicit API for an extension to execute code in the main world from a USER_SCRIPT world (or ISOLATED world). Apart from bikeshedding, I think this is fairly straightforward.
  3. A mechanism for USER_SCRIPT worlds to fetch or receive data from blobs.
  4. A secure communication channel for extension scripts to use between worlds.
  5. Establish common behaviors for the CSP of scripts injected into the main world by an extension (it seems there's some difference in how this works between browsers today). This ties into @tophf 's request for "create link, script, style elements even if their src or href or contents violates CSP of the page so that the users don't have to nuke the site's CSP header altogether."
  6. Provide a way for the USER_SCRIPT (or ISOLATED) world to directly access the main world (e.g. via an iframe contentWindow-like object).

Some of these will take more work and discussion than others (and I'm honestly not sure how feasible 6) is), but I think it's worth discussing all of them.

With this, we'd unblock this first iteration of the userScripts API, enable USMs to migrate to MV3, and have a road forward to future improvements where the browser can take progressively more responsibility for the USM injection.

Does that sound reasonable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rdcronin Your proposal looks great! Would you mind creating a new PR with your proposal? Then we can close/archive the current state of this PR, along with the discussion and very useful thread here for future reference.

In the update, could you also explicitly mention the desired behavior with regards to:

  • persistence of the registrations (e.g. persistAcrossSessions). I guess that the behavior is to persist for as long as possible?
  • order of execution: is it the order in the order of registration? Is the order reflected in getScripts()? What happens when modified by update()? If needed, future work could be to add a method to re-order scripts (if desired by USM).

Other remarks:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This discussion is addressed in the latest commit.
Resolving this discussion for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've marked this thread as unresolved, because otherwise the permalinks in the ToC are broken. I'd like the discussions to be easily referenced because the arguments and discussion are significant in the design and shape of the final API design.


#### B. The ability to execute code (with a separate CSP) in a world different from the main world and the extension's isolated world

##### `USER_SCRIPT` World

- Exempt from the page's CSP, can customize its own CSP (e.g can allow injection of `unsafe-inline`).
- Can communicate with the extension using extension messaging APIs (expanded under [Messaging](#Messaging)).
- Isolated from the web page (similar to other isolated worlds), but will be largely un-permissioned. It will not have access to any extension APIs, except messaging APIs mentioned, and will not have any cross-origin exceptions (these don't exist in Chrome, but do in other browsers). In the future, we may bring more APIs (such as chrome.dom APIs or a dedicated API to execute a script in the main world), but these APIs will not grant any additional privilege to the script beyond what it has to access the page content.
- Can communicate with different JS worlds via `window.postMessage()`. A dedicated API to communicate between worlds is being considered as part of future work.
- Shares DOM with the web page. 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 `USER_SCRIPT` world

##### Configuration

```
browser.userScripts.configureWorld({
csp?: string,
messaging?: boolean,
})
```
where
EmiliaPaz marked this conversation as resolved.
Show resolved Hide resolved
- If csp is defined, it is used in the `USER_SCRIPT` world. Otherwise, the `ISOLATED` world CSP is used.
- If `messaging` is true, messaging APIs are exposed. Otherwise, if false or undefined, messaging APIs are not exposed.
- Configuration persists across sessions.

In the future, if we allow multiple user script worlds (see section in Future Work below), this method can be expanded to allow for a user script world identifier to customize a single user script world.
Rob--W marked this conversation as resolved.
Show resolved Hide resolved

##### Messaging

User scripts can send messages to the extension using extension messaging APIs: `browser.runtime.sendMessage()` and `browser.runtime.connect()`. We leverage the runtime API (instead of introducing new userScripts.onMessage- and userScripts.sendMessage-style values) in order to keep extension messaging in the same API. There is precedent in this (using the same API namespace to send messages from a different (and less trusted) context, as `chrome.runtime` is also the API used to send messages from web pages.
Extensions can receive messages from user scripts with new event handlers: `browser.runtime.onUserScriptMessage()` and `browser.runtime.onUserScriptConnect()`. We want new events instead of using `browser.runtime.onMessage()` to make it clear the message is coming from a user script in a less-trusted context. There is precedent for this in the form of onMessageExternal and onConnect external.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other messaging APIs are attached to runtime because there is no more fitting namespace where they could be added. Despite the runtime API being available to every extension context (including the lower-privileged ISOLATED world), the intent is to only expose these events to privileged extension contexts, right?


#### C. A separate user script permission

- A new extension permission will be added, and will be required in order to register user scripts. This permission should be scoped specifically to the purpose of user scripts (as opposed to a general "remotely-hosted code" permission).
- Browser vendors then can use it to:
- Inform their review pipeline that this is a particularly risky extension, and extra caution should be taken to ensure this is a valid use of the API.
- Reduce the likelihood of extensions using it for other purposes and allows browser vendors to act on any permissions that do as being non-compliant
- Present the user with different permission warnings, UI, or other gating, if they deem it necessary.

#### D. Communication between JavaScript worlds

As mentioned in requirement A, the user script world can communicate with different JS worlds via [`window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) and DOM APIs. New communication methods are being considered as potential future enhancements. This is clunky and imperfect, but allows extensions to migrate to MV3 leveraging the same logic they have in MV2 today. New, dedicated communication methods are being considered as potential future enhancements.

### Other considerations

- When multiple scripts match and have the same runAt schedule, the execution order is:
- Scripts registered via the content_scripts key in the manifest file
- Scripts registered via [`scripting.registerContentScripts()`](https://developer.chrome.com/docs/extensions/reference/scripting/#method-registerContentScripts), following the order they were registered in. Updating a content script doesn't change its registration order.
- Scripts registered via `userScripts.register()`, following the order they were registered in. Updating a user script doesn’t change its registration order.
- User scripts are always persisted across sessions, since the opposite behavior would be uncommon. (We may explore providing an option to customize this in the future.)

### Browser level restrictions

From here, each browser vendor should be able to implement their own restrictions. Chrome is exploring limiting the access to this API when the user has enabled developer mode (bug), but permission grants are outside of the scope of this API proposal.

## (Potential) Future Enhancements

### `USER_SCRIPT`/ `ISOLATED` World Communication

In the future, we may want to provide a more straightforward path for communication with a `USER_SCRIPT` world (as opposed to the [`window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) approach highlighted above).

### Separate `USER_SCRIPT` Worlds

In addition to specifying the execution world of `USER_SCRIPT`, we could allow extensions to inject in unique worlds by providing an identifier. Scripts injected with the same identifier would inject in the same world, while scripts with different world identifiers inject in different worlds. This would allow for greater isolation between user scripts (if, for instance, the user had multiple unrelated user scripts injecting on the same page).

### Execute user scripts one time

Currently, user scripts are registered and executed every time it matches the origin in a persistent way. We may explore a way to execute a user script only one time to provide a new capability to user scripts (e.g `browser.userScripts.execute()`).
EmiliaPaz marked this conversation as resolved.
Show resolved Hide resolved

### Establish common behaviors for the CSP of scripts injected into the main world by an extension

Create certain HTML elements even if their src, href or contents violates CSP of the page so that the users don't have to nuke the site's CSP header altogether.

### Dedicated API to execute code in the `MAIN` world from another world

Provide an explicit API for an extension to execute code on the main world from a `USER_SCRIPT` or `ISOLATED` world.

### Direct access to the `MAIN` world from another world

Provide a way for the `USER_SCRIPT` or `ISOLATED` world to directly access the MAIN world (e.g. via an iframe contentWindow-like object).

## Discussion Guidelines

This is the first iteration of the userScripts API designed in https://github.com/w3c/webextensions/issues/279.
Future enhancements can be tracked in the WECG [issue tracker](https://github.com/w3c/webextensions/issues).