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

Proposal: provide a way to register a "page script" #85

Open
ameshkov opened this issue Sep 20, 2021 · 18 comments
Open

Proposal: provide a way to register a "page script" #85

ameshkov opened this issue Sep 20, 2021 · 18 comments
Labels
implemented: chrome Implemented in Chrome proposal Proposal for a change or new feature supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari

Comments

@ameshkov
Copy link

ameshkov commented Sep 20, 2021

The root problem is that since MV3 forbids eval, there's no way to inject a page script that would work before the page scripts.

Check out the examples below:

MV2 example

content_script.js:

    const scriptTag = document.createElement('script');
    scriptTag.setAttribute('type', 'text/javascript');
    scriptTag.innerHTML = 'console.log("I will be started BEFORE the page scripts.")';
    (document.head || document.documentElement).appendChild(scriptTag);

MV3 example

script.js:

console.log("I may run AFTER some of the page scripts.");

content_script.js:

    const scriptTag = document.createElement('script');
    scriptTag.setAttribute('type', 'text/javascript');
    scriptTag.setAttribute(chrome.runtime.getURL('script.js'));
    (document.head || document.documentElement).appendChild(scriptTag);

Setting scriptTag.async = true should probably solve the scripts execution order, but what if we need the page script to be parameterized? We'll have to establish a communication channel between the content script and the page script which will be asynchronous and inevitably postpone the execution.

Proposal

It would help a lot if we had an API similar to chrome.scripting.executeScript, but for executing scripts in the page's context and available to content scripts. This kind of API can also be a solution to other issues mentioned in this repo (see #77 #78), the injected script's context may be enriched with a "communication" object.

Use cases

Content blockers like AdGuard, uBO, and ABP have an option to run the so-called "scriptlets" (see an example here). The timing is crucial for these scriptlets.

Let's take an example:

  • abort-on-property-read: aborts a script when it attempts to read the specified property.
  • Example rule: [many domains...]#%#//scriptlet("abort-on-property-read", "BetterJsPop"). Example website: gledajcrtace.xyz. This rule blocks a very popular pop-up ad script. And as I've said, the timing is crucial for this scriptlet to work properly, otherwise we may not be able to define the property before it's initialized by the page.

The current workaround solution for MV3:

1. Create a hidden DOM element and put the data necessary for the scriptlets initialization inside
2. Inject the page script as a script src="web accessible resource"
3. The page script finds that hidden DOM element, evaluates its content, and removes it from the DOM.

The proposed solution:

chrome.scripting.executePageScript(
    {
        target: window,
        func: abortOnPropertyRead,
        args: [ 'BetterJsPop' ],
    },
    (injectionResults) => { /* handle result here */ }
);
@Jack-Works
Copy link

Yes, the ability to interact with the main world script is important.

For example:

  • MetaMask injects a global Ethereum object to provide a digital wallet API.
  • Grammarly replaces EventTarget.prototype.addEventListener to implement the necessary functionalities.

@tophf
Copy link

tophf commented Sep 28, 2021

This is already planned in Chrome's MV3, https://crbug.com/1054624.

@ameshkov
Copy link
Author

@tophf dynamic content scripts while being an awesome addition, are a completely different thing. I am talking about scripts that are executed in the context of a page.

@tophf
Copy link

tophf commented Sep 28, 2021

As you can see in the linked bug comments, the ability to inject scripts in the main world of the page will be a part of the API.

@ameshkov
Copy link
Author

Oh, this is awesome, thank you!

@ameshkov
Copy link
Author

Moreover, I see that the world argument is now added to chrome.scripting.executeScript. This basically implements everything that was proposed in this issue.

@WebReflection
Copy link

I can confirm both isolated and main worlds work as expected, as well as arguments passed along, and returned values.

The only weird thing during my tests is that explicitly throwing within the callback doesn't reject, but it early returns ... not sure if it's meant or a bug, but I wouldn't mind returning errors and check these, although I am not sure these survive the structured serialization once returned.

@tophf
Copy link

tophf commented Sep 28, 2021

executeScript returns the last evaluated expression, it doesn't care if it's early or not and executeScript doesn't use the Structured Clone algo, it uses JSON serialization just like it did in MV2 so only the few basic types are supported.

@WebReflection
Copy link

@tophf thank you for the extra clarification. I have noticed that postMessage uses that Structured Clone algo though, so I was wondering if it could make sense to align executeScript so that we could pass, and return, slightly more complex values?... or maybe that's a performance issue/concern? Thanks again.

@tophf
Copy link

tophf commented Sep 28, 2021

Someone should probably open a new issue here to suggest that extensions must use the structured clone algo both for executeScript and extension messaging. Firefox already uses this algo for the latter, IDK about executeScript. I also think that executeScript should automatically resolve Promise inside the injected code.

@apple502j
Copy link

Will there be a way to register the "main-world" scripts in manifest.json? The API provided by chrome.scripting requires tab ID, which needs to be fetched separately.

In manifest v2, inline <script> in a document_start content script almost always guaranteed that the script would run before any scripts in the page. With the current API we need to 1) wait for SW to wake up, 2) grab the tab ID, and 3) run the code, and I'm worried the scripts in the page could run before the injected code.

Example of document_start MV2 content script that does "ASAP loading":

const script = document.createElement("script");
script.textContent = "window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = function(){ /* blah */ }";
(document.head || document.documentElement).append(script);

@tophf
Copy link

tophf commented Feb 24, 2022

@apple502j, MV3 restriction on inline scripts can be trivially circumvented via el.setAttribute('onclick', 'your code for the main world'), adding the element into DOM, followed by el.click()

@Jack-Works
Copy link

@apple502j, MV3 restriction on inline scripts can be trivially circumvented via el.setAttribute('onclick', 'your code for the main world'), adding the element into DOM, followed by el.click()

but that doesn't bypass the CSP

@alesandroortiz
Copy link

For any lost souls out there (like myself) looking for a solution, early main-world script injection is supported in Chrome since 102.0.5005.61 Stable per https://chromiumdash.appspot.com/commit/e5ad3451c17b21341b0b9019b074801c44c92c9f and https://bugs.chromium.org/p/chromium/issues/detail?id=1207006#c9

@rektide
Copy link

rektide commented Jul 30, 2022

does main-world script injection still get hit by csp?

@danielcaldas
Copy link

does main-world script injection still get hit by csp?

Yes, it does.

@tophf
Copy link

tophf commented Jul 30, 2022

The MAIN world script can run in pages with CSP that forbids inline js, so theoretically it can be said the CSP of the page was circumvented to allow running this code. However, the script can't use eval and create dynamic functions if those are forbidden by page CSP. The DOM things created by such script like link, script, style also obey the CSP of the page.

@xeenon xeenon added the proposal Proposal for a change or new feature label Aug 31, 2022
@Rob--W Rob--W added implemented: chrome Implemented in Chrome supportive: firefox Supportive from Firefox labels Sep 23, 2022
@Rob--W
Copy link
Member

Rob--W commented Sep 23, 2022

Firefox has not implemented this yet, but it's tracked at https://bugzilla.mozilla.org/show_bug.cgi?id=1736575

@xeenon xeenon added the supportive: safari Supportive from Safari label Sep 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
implemented: chrome Implemented in Chrome proposal Proposal for a change or new feature supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari
Projects
None yet
Development

No branches or pull requests

10 participants