Adding external script in an extension

We have an extension that allows the user to re-write, remove or add in their script they have with our business. This has been live and approved for about 7 years.

Recently we have worked on building additional capability with our other customer offering, but when submitting the version we get pushback on the reviewers on the part that allows the user to inject their script.

This is frustrating as it is already live but this restriction is stopping us making any version updates again without removing this old but still necessary functionality.

Can anyone recommend a way to add an external script to the parent page that is acceptable to the extension reviewers?

There is a special API called userScripts for this purpose:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts

Thnks for the link. The script in question is dynamic depending on the details entered into the extension fields, so cannot be implicitly stated in the userScripts list. Also the script has to be loaded in the parent page as the script will drop essential code for that customer needs to have in their browser for the solution to work. Anything else you think could help here. Currently of course we are using the browser.tabs.executeScript function in the background.js but that is what the reviewers will no longer allow like before. So need an alternative that does the same thing but will get passed the reviewers eyes.

The userScripts is for dynamic scripts, check the API:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/register#parameters
You can pass string there and it will execute it. But it will not get access to the web-extension API.

If you want your users to use some of the web-extension API, you have to expose it manually using some commands.
The point is to make things predictable - so that your addon cannot turn malicious simply by loading remote script that gets executed with the full WebExtension API access.
So you just have to think the worst case scenario and make sure it’s not possible and then program it in way that a reviewer can see that too.

As you can probably tell, this is my first dabble with extensions, so getting to these niche use-cases to comply with FF’s stricter rules (My Chrome update is already approved and live) is proving to be a step ahead of my knowledge.

The code currently will inject a script HTML tag with the src being what the user enters for the host and initial path file directory and then the end is hard-coded to always be ‘/Bootstrap.js’. Can you share how you would go about using this API to achieve that and what parts of the manifest and background.js or popup.js needed updating with what code?

I know the rules are strict and cause issues to us - developers. But it’s here to protect everybody else. Just read this article about malicious extensions that were working for years without anyone noticing:


(it’s a long deep dive analysis, but very interesting reading)

Regarding your problem, all you need to do is execute this in the background script:

const registeredUserScript = await browser.userScripts.register({
  matches: ["https://*.example.com/*"],
  js: [{code: `console.log('hello world');`}]
});

Then the code you provide as a string will be injected into the page matching the URL.
You need to execute it only once - then it’s fully automatic, when user loads that page, it will be injected there.

Regarding your manifest file:

This API requires the presence of the user_scripts key in the manifest.json, even if no API script is specified. For example. user_scripts: {} .
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/user_scripts

1 Like

I don’t understand how this is any safer as I will still be injecting a script via this method right? Plus the matches index will have to be a regex for any page as the extension currently will inject the script on all pages you land on until you change it or turn that feature off. I don’t see how this approach is safer, surely the reviewers would still flag this as a security risk given my requirements?

It’s all explained in the documentation, in the first link I’ve posted here. You can read there this:

This API offers similar capabilities to contentScripts but with features suited to handling third-party scripts:

  • execution is in an isolated sandbox: each user script is run in an isolated sandbox within the web content processes, preventing accidental or deliberate interference among scripts.
  • access to the window and document global values related to the webpage the user script is attached to.
  • no access to WebExtension APIs or associated permissions granted to the extension: the API script, which inherits the extension’s permissions, can provide packaged WebExtension APIs to registered user scripts. An API script is declared in the extension’s manifest file using the “user_scripts” manifest key.

As I’ve mentioned before, the problem is that if your addon can execute string expression, it can easily download a remote script (as text) and execute it with full Web Extension access.

This API restricts the dangerous parts so the reviewer will be happy because he will know that the addon is harmless now.

Also, no need to use regex, you can use a full URL. See the docs for more info:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
And the register function returns object with unregister function so you can switch it off.

Thanks, I have been playing around but not having much success really. I keep getting this value below in the inspect view of thebackground.js:

Uncaught SyntaxError: await is only valid in async functions and async generators background.js:74:29

I played around in that console directly and eventually got something working, but it was firing the script numerous times on each page load which is not correct. Perhaps I am missing something here. Do you any further suggestions on how to adapt the code?

I have "user_scripts": {} within my manifest and the below in my background.js file:

const registeredUserScript = await browser.userScripts.register({
  matches: ["<all_urls>"],
  js: [{code: `link=document.createElement(\'script\'); link.src=\'https://' + host + '/' + localStorage.clientId + space + '/Bootstrap.js\';document.getElementsByTagName(\'head\')[0].appendChild(link)`}]
});

The “await” can be used only in a function marked as “async”.
It’s a syntactic sugar to make work with promises easier. In general you just have to add word “async” in front of the word “function” and it will work.

See these examples:
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await

More info:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

Ah gotcha, that seems to have done the trick after doing some additional tweaking on top of that with my specific use-case. Hopefully it passes the next review, thanks for your help and tips.

1 Like

@juraj.masiar I have one more hurdle I have found. It all works fine, when I unregister it when the user toggles the mode off it stops, but then when it is re-enabled (and therefore run the register again) I get duplicate scripts firing. I tried dabbling with having the register against a window variable in the background script and checking it, but I am struggling to get it to work when I start playing with it. Do you have any suggestions as to how I can register and unregister (or something similar to suppress and allow it regularly) it without duplicates?

What do you mean by “duplicate scripts firing”.
Do you get some runtime error saying “SyntaxError: redeclaration of const”?

In cases when you need to inject same script into same page multiple times, you have to avoid declaring global constants. Re-declaring function is not a problem.

To avoid this, you can simply wrap whole script into IIFE.
For example:

(() => {  // this is Immediately Invoked Function Expression
  // place your code here
})();

Or by using some “main” function:

function main() {
  // place your code here
}
main();

JavaScript has “function scope”, so things inside a function (like constants and functions) are encapsulated inside and not visible to the outside world.

@juraj.masiar Below is the kind of thing I am doing currently (with some stuff removed that is not relevant):

First I have a function that fires the inject code. This is it’s own function because it gets triggers by a listener that triggers when the option is changed to ‘Inject’:

async function insertBootstrap(e) {
		if(!window.insertBootstrapReg){
				window.insertBootstrapReg = await browser.userScripts.register({
						matches: ["<all_urls>"],
						js: [{ code: 'link=document.createElement(\'script\'); link.src=\'https://' + host + '/' + localStorage.clientId + space + '/Bootstrap.js\';document.getElementsByTagName(\'head\')[0].appendChild(link);' }]
				});
		}else{
				window.insertBootstrapReg.register({
					matches: ["<all_urls>"],
					js: [{ code: 'link=document.createElement(\'script\'); link.src=\'https://' + host + '/' + localStorage.clientId + space + '/Bootstrap.js\';document.getElementsByTagName(\'head\')[0].appendChild(link);' }]
				});
		}
}

Then I have this code to unregister when the optioin is changed or the tool is turned off:

if(window.insertBootstrapReg){
	window.insertBootstrapReg.unregister();
}

This seems to work until you turn it back on after turning ‘Inject’ off. Then it no longers works, I get no console errors or anything like that. I suspect it is because the window variable is alreayd there. I did have this slightly different before but on every page load an extra script is added, so the new register on each event caused duplicates (that is why I added the window variable to stop that). But once it is unregistered I cannot reinstate it without getting duplicates.

Is there a way to almost “re-register” the initial script when it is re-toggled by the user or something similar?

This is bit messy… you should avoid adding custom properties to the window object.
Instead use local variables using “let” or “const”. Also what is a “link” in the code you are executing, shouldn’t that be “var link”? You can also use ` instead of ’ so that you don’t have to escape those apostrophes.

Anyway you should be able to debug this code from the background scrip inspector (when you click “Inspect” button in the “about:debugging#/runtime/this-firefox” page).

And one last thing - you are actually executing remote code???
No wonder reviewer rejected it :smiley:, remote code execution is totally forbidden.
It’s allowed in Chrome for now, but it won’t be anymore in the new Manifest V3..

But it should be fine here as long as you use the user script API (otherwise all those user-script addons wouldn’t be able to sync your scripts).

@juraj.masiar Like I said, using const or let didn’t work as the unregister action is in a different fucntion, so there is a scoping issues. I don’t really want to use window variables but struggling to see what other options I have.

I think my code works okay in general (so no debugging necessary), I just need to understand if (and hopefully how) I can re-register a userScript once it has it has been un-registered. Or, failing that; know a way to clear off all the existing userScript so it cannot re-trigger again so I can add it in afresh when the user toggles the feature back on. That is my only remaining issue is that when I un-register and then add a new register action it fires the script twice, then if I disable and re-enable the feature another time then I get three hits and so on.

Yeah we are actually firing remote code, but it is a secure script for our technology only. I get why they are restricting it, but as we already have had this feature for many years that our customers rely on, we want to ensure we can keep this option available to them for as long as possible.

The “unregister” should kill the script itself, so no event handlers defined by the script will run. However all changes done to the DOM will be still there. This includes the remote script tag you add there. So that script will be most probably still running even after unregister.

So you need to make sure the remote script can be executed multiple times in the same context.
You also may need to implement some “kill” command that will stop the remote script execution (whatever the script is doing). Maybe send it a message with postMessage.

@juraj.masiar Right, so once the userScript is run the first time, then there is no way with extension functions to stop it firing even when unregistered?

That could be a deal-breaking with using this method as we need to be able to toggle the freature on and off at will. As we have other functionality that re-writes existing script requests we would need to have a lot of logic to ensure those features aren’t hindered by a kill switch against that domain.

I just need to way to add a script to the parent page at the will of the user if and when they use our ‘inject’ capabilities.

Is there anything else you can suggest with extension code that I can do to get around my issue here?

Actually, if all you need is execute a remote script, then you should just download the script as text and register it directly.
Something like:

const code = await (await fetch('https://...')).text();
await browser.userScripts.register({js: [{code: code, ...}]});

@juraj.masiar That is a nice option to have, but it still doesn’t get by my issue where I need to turn the functionality on and off at the will of the user. As they can enter some data dynamically with fields, we can’t even cache or store the URL used as it could change. Plus we have proven that having multiple registers causes multiple scripts firing incrementally.

I need a way to re-register a user script once it has been unregistered or have a way to completely remove the existence of a user script when you unregister it. Do you have any suggestions for that?