Open rdcronin opened 3 months ago
A message port can be passed to another world [...] by leveraging the new dom.execute() API
But this won't support userscripts registered via userScripts.register() API in the target world (e.g. "main"), so we will have register them in the primary userscript world and then run them via dom.execute(), which is pretty wasteful due to the additional serialization/deserialization and various other JS checks.
Possible solutions:
runAt: 'manual'
window.randomId
in the main world using the specified random id, the same random id would be embedded in the code of the main-world userscript so it'll read the value synchronously, delete window.randomId, and use it to establish the secure channel.It's also imperative to have the ability to pass DOM nodes (live and detached, from either a shadow or the main tree) and a window
object. The recipient receives its own world's version of the object, of course. Currently ***monkey extensions are doing it by dispatching two messages: CustomEvent with the data and a MouseEvent with relatedTarget set to the node/window (the actual supported type is EventTarget
).
❌ It means that a MessagePort cannot be used.
sendNode
method in addition to the main send
method. Today, to communicate between JS worlds, extensions can use custom events [...] This is fragile and hacky, and can lean to leaking more data to the embedding page.
To clarify, it's not that using CustomEvent
per se is fragile and hacky, but using any built-in global or prototype is unsafe (e.g. MessagePort) due to a bug in all browsers that allows the embedder to poison the prototypes of a same-origin iframe before document_start content script runs inside. There's also the problem of passing the random event id between worlds to avoid eavesdropping by other extensions or the web page scripts, although it's trivially solved by the upcoming dom.execute() in #678, which only leaves the problem of poisoning.
There is a way to use CustomEvent mechanism safely for a communication channel - just don't depend on current prototypes, i.e. addEventListener, dispatchEvent, CustomEvent constructor, CustomEvent.prototype.detail getter all should be used from a safe source, not from the current JS environment.
Current workaround for extensions is to extract these safe original functions from a temporary iframe that they create in their secure world using a function extractor that runs synchronously in the target world ("main").
Browsers should be able to do it without creating an iframe:
In ether case the object produced by dom.createPort() will use these functions internally.
One inconvenience of CustomEvent is the need to have a target such as window
, which means that the event listener can be seen in devtools inspector's event listeners panel. While not a safety concern, but an ideal solution would be to use the CustomEvent mechanism directly without attaching a visible event or mark this event listener as invisible for devtools and debugger protocol.
Compiling the reasons why MessagePort cannot be used as the internal mechanism:
Note: "MessagePort" in this proposal refers to a new type. It is not the MessagePort from the web platform. I explicitly called out before that this concept already exists and that it cannot be used to serve the use case of extensions, at https://github.com/w3c/webextensions/pull/679#discussion_r1754969359
Regarding the prototypes: I'd like the proposed API to be safe against prototype tampering. This was called out at https://github.com/w3c/webextensions/pull/679#discussion_r1754990101 . There is also a special note on (un)safety of received values, at https://github.com/w3c/webextensions/pull/679#discussion_r1755098971
"MessagePort" in this proposal refers to a new type
Then it must be renamed, otherwise it'll be the "Local Storage" disaster all over again, when new developers confuse chrome.storage.local with localStorage en masse and mix up their usage patterns, limitations, availability, permissions, and inspectability in devtools.
It's unclear to me that it should contain "port" in its name.
What is clear to me is that it should be based on the existing robust synchronous mechanisms such as CustomEvent (for objects) and MouseEvent (for DOM nodes), which aren't "fragile or hacky" per se as I explained in my comments above in detail.
It's also unclear if dom.createPort() should be a separate thing, because it seems to be only useful as a part of dom.execute().
The addListener/removeListener/hasListener part seems an overkill.
Exceptions/errors:
args
can't be serialized before executionCurrently the API would have to throw a hard exception in both cases, meaning that the API can't be used without wrapping it in try/catch, which is not obvious and is rather inconvenient.
A pre-existing port makes it possible to send messages into nowhere before it's connected to the world and this should be reported somehow e.g. by throwing a synchronous exception or via chrome.runtime.lastError, both are very inconvenient.
A simpler solution might be to incorporate it all in dom.execute.
port
parameter and resultonmessage
parameteronmessage
parameter + port
result
This adds a proposal for a new
dom.createPort()
method, which will allow communication between different JavaScript "worlds". It leverages the proposeddom.execute()
method (#678).