-
Notifications
You must be signed in to change notification settings - Fork 56
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
Conversation
LGTM Co-authored-by: Simeon Vincent <simeonv@google.com>
Commit 875c2ea wrongly deleted the API schema. This commit adds it back
From what I can tell. It still doesn't fulfill the needs of userscript managers but now it's enough to make userscript managers possible, albeit not feature parity with what firefox already provides |
Hello @brunoais |
Reminding just in case, this new proposal is still not usable in Tampermonkey or Violentmonkey as it doesn't address the problem of secure communication between worlds. If there is actually a workaround, it should be mentioned in the proposal e.g. maybe we can embed the secret key inside the userscript code and re-register it each time, but AFAICT it won't work because we can't register code for the isolated world to pass this key to extension's content script (both sides need the same secret). And there's still no MV3-compatible solution against poisoned environments when injecting in the MAIN world. |
@EmiliaPaz: tophf already summarized it. |
proposals/user-scripts-api.md
Outdated
- Isolated from the web page (similar to other isolated worlds), but will be un-permissioned. It will not have access to any extension APIs or cross-origin exceptions (these don't exist in Chrome, but do in other browsers). | ||
- Exempt from the page's CSP (see [Relax CSP](#relax-csp)). | ||
- Share DOM with the web page. Code from both worlds cannot directly interact with each other, except through DOM APIs. | ||
- Can communicate with different JS worlds via [`window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). A dedicated API to communicate between worlds is being considered as part of future work. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
window.postMessage
should not be recommended. It can easily be eavesdropped + intercepted by the web page (see #77).
To communicate between worlds, CustomEvent
can be used (Ctrl-F CustomEvent at #279 (comment)), but that is only secure if the two scripts (user script + privileged side, e.g. content script) can keep a secret. For the USER_SCRIPT world, this is somewhat possible, because web pages cannot access the USER_SCRIPT world.
For the MAIN world, this requires the user script to both have access to the secret (unique per execution of the script, so it cannot be added with the proposed script registration method!) AND more importantly: DOM APIs that have not been tampered with.
When a content script controls the script execution, it can use existing DOM APIs to try and set up a secure environment with such access, but that is not possible when the browser controls the injection without alternative.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In defence of the current proposal
window.postMessage
has the argument targetOrigin
.
Maybe it can be made use of by using that argument to specify the exact userscript.
Using an Event
is also a bad idea:
- It's synchronous to pass data
- There's no way to respond to a request. Instead it's close to being in a UDP message passing system.
- There's no delivery guarantees (well... postmessage() also doesn't but it's a bad thing of both)
- There's no way to pass data without serialization (no transferrable content allowed)
- This can be horrible for
XMLHTTPRequest
orfetch
performance
- This can be horrible for
So I propose:
If you want to present an alternative, present MessagePort
!
Asynchronous message passing which has transferrable capabilities and the destination is the owner of the other side of the port.
Conceptually simple that solves all of these issues except this one:
How to deliver the port to each side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In defence of the current proposal
The proposal doesn't provide secure way of using any kind of messaging so you're not really defending it. Even with postMessage origin, we can't target userscripts in the main world as their origin is not unique. Also, when such a main world's userscript sends a postMessage to the extension's content script in the isolated world, the extension can't be sure that the message wasn't faked by a hostile unrelated script.
It's synchronous
Userscript managers such as Violentmonkey/Tampermonkey still need it to implement the classic Greasemonkey API, which is used by the majority of userscripts, many of which will never be updated. There are many other cases when a synchronous response is required, which is why CustomEvent exists on equal terms with asynchronous methods.
There's nothing bad about synchronous communication because the actual internal work under the hood is identical for both asynchronous and synchronous methods, the only reason why postMessage or MessageChannel are asynchronous is because they are designed for cross-origin communication where environments use a different OS thread/process. In the same JS environment postMessage may be effectively slower than CustomEvent due to the former being much more complicated inside and the requirement to deliver the message in a separate event loop task per the specification.
There's no way to respond to a request
Simply send a message back immediately (it's synchronous as well) or add an id to every message.
There's no delivery guarantees
The extension has added a listener, so there is a guarantee.
There's no way to pass data without serialization
Indeed - in case you mean structured cloning - but passing a Blob of any size takes just 1ms via CustomEvent or postMessage because blobs are pointers to data not the data itself. To pass other binary types of data we can wrap it in a Blob, probably.
How to deliver the port to each side?
That's the essence of the problem: the API should provide a secure method of communication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tophf : Fair enough 🙂
#### 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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):
- Relax the current MV3 CSP requirements in the extension
ISOLATED
world by accepting 'unsafe-inline' too, or - 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. 😁
There was a problem hiding this comment.
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:
- 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 afile
parameter to the registered script, to reflect how it is more likely to be used at first). - 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 }
. Ifcsp
is undefined, the isolated world CSP is used; ifenableMessaging
is undefined or false, no extension APIs are exposed. ifcsp
is defined, that CSP is used in the USER_SCRIPT world. IfenableMessaging
is true, we exposechrome.runtime.sendMessage()
andchrome.runtime.connect()
. Here, I'm suggesting theruntime
variants here, instead of something likeuserScripts.sendMessage
, to align more with the existing pattern used by both web pages and content scripts; I'm open to bikeshedding this. I suggestenableMessaging
(and notenableAPIs
) to account for more flexibility in the future, e.g. whether the extension may want to control if thedom
API is exposed. - Introducing a new event for receiving messages from user scripts (similar to the prior art of
runtime.onMessageExternal
).userScripts.onMessage
anduserScripts.onConnect
make sense to me, but I could see an argument for following the pattern of runtime here, too, and introducing aruntime.onUserScriptMessage()
andruntime.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:
- Allow user scripts to inject in unique worlds. I'd propose we do this by the introduction of a
worldId
inuserScripts.register()
; user scripts with the sameworldId
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 aworldId
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. - 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.
- A mechanism for USER_SCRIPT worlds to fetch or receive data from blobs.
- A secure communication channel for extension scripts to use between worlds.
- 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."
- 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?
There was a problem hiding this comment.
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 byupdate()
? If needed, future work could be to add a method to re-order scripts (if desired by USM).
Other remarks:
- The current version lists one
code
(and maybe onefile
). Would it make sense to support a list of scripts, e.g.js: [ {file: "first.js"}, {code: "console.log('2nd');" }]
? The latter is used in Firefox's existinguserScripts.register
method at https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/register#parameters (but I'm willing to consider a different shape).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
||
### 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. |
There was a problem hiding this comment.
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).
string[]? excludeMatches; | ||
string id; | ||
string[]? matches; | ||
RunAt runAt; |
There was a problem hiding this comment.
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.
Addresses multiple comments on the previous proposal, specially [comment](w3c#331 (comment)) Main change is introduction of messaging between `USER_SCRIPT` world and extension
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving PR to reflect that the general shape of the API looks like it's worth prototyping, especially with the planned future work. I added a few more comments, and expect that it can be merged at the next update.
proposals/user-scripts-api.md
Outdated
// Errors if both code and file are specified. | ||
dictionary ScriptSource { | ||
string code; | ||
string file; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specify file
to be a relative URL; relative to the extension root? (opposed to an absolute URL or a URL relative to the current execution context).
##### 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. |
There was a problem hiding this comment.
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?
#### 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) |
There was a problem hiding this comment.
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.
proposals/user-scripts-api.md
Outdated
|
||
##### 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it makes more sense to add a new namespace for messaging with the userScripts
API, instead of runtime.sendMessage
/connect
, for the following reasons:
- If future work introduces additional functionality, it would need a namespace to hold the methods. We may as well introduce the namespace now, e.g.
userScriptContent
. - In this API, eventually code will be running in the
MAIN
world and theUSER_SCRIPTS
world. It may be confusing ifruntime.sendMessage
/connect
fromMAIN
triggersruntime.onMessageExternal
/runtime.onConnectExternal
, while exactly the same code triggersruntime.onUserScriptMessage
/runtime.onUserScriptConnect
inUSER_SCRIPT
(... andruntime.onMessage
/runtime.onConnect
inISOLATED_WORLD
through content scripts, e.g. thescripting
API).- I already see frequent confusion among developers who do not understand the difference between the
runtime.sendMessage
/connect
APIs in web pages vs content scripts, where the observable difference is that the former requires anextensionId
parameter.
- I already see frequent confusion among developers who do not understand the difference between the
- The
extensionId
parameter allows communication with other extensions. Since the method is explicitly meant to be used by user script managers, I don't think that this would be desirable. This could easily be addressed at the implementation level by rejecting theextensionId
parameter, but good luck with documenting that...
What do y'all think of these APIs instead:
browser.userScriptContent.sendMessage
/connect
browser.userScripts.onMessage
/onConnect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly feel we should not add yet-another namespace for this. I think documenting the differentiation between userScriptContent and userScripts would be extremely difficult. I also think that having the API go from two different namespaces (userScriptsContent.sendMessage -> userScripts.onMessage) is very confusing.
I don't think that reusing the existing runtime methods is perfect, either — I think each have their challenges, but because IMO there's no clear right choice, I'd prefer to go for the one that is more consistent and involves fewer pieces.
No matter what, there will be some confusion — having the messaging API exposed on a different namespace makes it different than sending a message from any other context, where you can always use chrome.runtime. In any of these cases, we'll need documentation, and developers will need to learn the different conventions. I prefer leveraging chrome.runtime because:
- It's more inline with existing messaging behavior, overall (extension frames / SWs and content scripts both use chrome.runtime to send messages)
- It avoids the need to introduce new API namespaces and the confusion that spans from that
- It's simpler implementation-wise, leading to less risk of skew between different messaging APIs
I also think there's good potential here with the getContexts() API and messaging changes we're planning there (allowing the extension to target a specific context) and this work — for sending messages from an extension frame or SW to a user script, we can leverage that API. Looking farther down the road, I think it'd make sense to have only runtime.sendMessage()
and runtime.connect()
and to use these with targeted contexts. I don't suggest we do this yet (it's a very breaking change), but I think we can take steps in that direction.
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Co-authored-by: Rob Wu <rob@robwu.nl>
Addressed comments made on 253ddc4 Changed userScripts.configureWorld parameter to `messaging`
@EmiliaPaz This is good to merge from my perspective. |
The only open discussion is messaging namespace. If you are okay with resolving this outside this proposal, then we are good to merge! |
User Scripts API proposal for MV3
This is the second revision of the initial proposal. Previous discussion at issue