How to Alter Third-Party Web Apps with Browser Extensions (Add-ons) without the Need to Sign and Publish Them

Sergejs Kozlovičs
5 min readSep 26, 2023

--

Assume you wish to embed a third-party web app into an <iframe> inside your web page or web app. However, you want the third-party app to behave slightly differently, e.g., add some <div> or JavaScript code, remove some annoying <img>, or just change the onclick behavior of some button. In most cases, you won't be able to modify the code of the third-party app directly. However, if you were a web browser, you could alter any web page (including the third-party web app) before displaying it to the end user. Can we do that without creating our own browser?

Yes, we can. Earlier, each browser had its browser-specific API for creating extensions that could enhance browser functionality. Today, most browsers support cross-browser WebExtensions API.

For a minimal browser extension, you will need two files: manifest.json (describing the extension) and, say, main.js (contatining the extension code in JavaScript).

Sample manifest.json:

{
"manifest_version":3,
"version":"1.0",
"name":"My Extension",
"content_scripts":[
{
"matches":["https://your.domain.org/*", "file:///*", "http://localhost/*"],
"js":["main.js"],
"all_frames": true
}
],
"browser_specific_settings": {
"gecko": {
"id": "test@example.org"
}
}
}

Notice that some browsers won't install extensions with manifest_version less than 3.

The matches setting contains a mask for URLs, for which the extension will be loaded by the browser. We have added file:///* and http://localhost/*, which may be useful for development purposes.

The all_frames setting is important if the web app to which you want to attach the extension will be loaded inside an <iframe>.

The browser_specific_settings specify the developer's e-mail. This setting is needed by Firefox but is not supported by Chrome :(

Sample main.js:

console.log("Extension loaded.");
// can use document.body right away!

Pitfalls

You may think we can now install the extension into our favorite browser, and everything will be fine. I am sorry, but there are several pitfalls…

Extensions need to be signed

For security reasons, browsers won't allow you to install just any extension because some could be fraudulent. If you are a developer and wish to deploy an extension, you must obtain a signature to publish your extension in Firefox store, Chrome Web Store, or App Store for Safari. For different stores, you will need different signatures! A nightmare!

For development purposes, you need to turn off signature checks

In the Firefox address bar, type about:config (don't worry, take the risk if Firefox asks). In the search box, type xpinstall.signatures.required and then double-click on the popup to change the value from true to false.

In Safari, you will need to enable the Develop menu by selecting the "Show Develop menu in menu bar" checkbox in Preferences → Advanced. Then validate that there is no tick in Develop → Disable Extensions. Then choose Develop → Allow Unsigned Extensions.

You will need to perform tambourine dances to load an extension for development purposes

In the Firefox address bar, type about:debugging. Then click the "Load Temporary Add-on" button and choose the manifest.json file of the extension (other files, e.g., the extension code, must be in the same folder where manifest.json is located). Notice that the extension will be available only temporarily (until the next Firefox restart).

In the Chrome address bar, type chrome://extensions/ and turn on the "Developer mode" switch. Then click the "Load unpacked" button and choose the folder where manifest.json and other extension files are located. However, Chrome won't load manifest.json that contains the browser_specific_settings JSON field (as in our example above), which is needed to install unsigned extensions in Firefox.

In order to feed your extension to Safari, you will need to launch it from Xcode first. Earlier, Xcode came with a tool that could convert extensions to Xcode format, but that tool seems not to work in recent versions of macOS and Xcode. It seems also that it is impossible to install unpublished extensions without Xcode.

Solutions to the Pitfalls

Solution 1: The developer’s version of Firefox

This is a not-so-good solution since we require our end users to stick to a specific variant of a particular browser. You will need to create an .xpi file, a zip archive containing the extension files, including manifest.json, in the root of the archive.

Although Firefox also supports .zip extension, .zip files are usually automatically extracted on macOS, but we need an archive.

Ask your end users to do the following:

  • Download the .xpi file.

We recommend using the .xpi file extension since macOS automatically extracts downloaded .zip files.

  • Download and install the developer's version of Firefox.
  • In the address bar of the developer's version of Firefox, type about:config (don't worry, take the risk if Firefox asks).
  • In the search box, type xpinstall.signatures.required and then double-click on the popup to change the value from true to false.
  • In the address bar, type about:addons, click the settings button (the cog), and choose "Install addon from file..."
  • Choose the .xpi you have downloaded in the first step.

Solution 2: "Userscripts"

The term "augmented browsing" refers to user experience when a certain "device" alters/enhances the web page. In our case, the "device" is a browser add-on that can execute "userscripts". The "device" is also called userscript manager.

Perhaps the best userscript managers are:

A userscript is JavaScript code that resembles traditional browser extensions but does not require signing and all those tambourine dances mentioned above (but requires an installed userscript manager).

Now, instead of manifest.json + main.js, we have just one file. Its name must end with .user.js, e.g., my-extension.user.js. When the user clicks (downloads) any file with the .user.js extension, the usersrcipt manager (e.g., Tampermonkey) will usually automatically offer to install it as a userscript.

Sample my-extension.user.js:

// ==UserScript==
// @name My Extension (user.js)
// @version 1.0
// @description My Extension
// @match http://your.domain.org
// @match file:///*
// @match http://localhost/*
// @grant none
// @run-at document-start
// ==/UserScript==

function init() {
console.log("Extension (.user.js) and the web page have been loaded");
// can use document.body
}



// For iframes:
if (window.location.hostname === "iframe.domain.com") {
init();
}
else {
// cannot use document.body!
document.addEventListener("DOMContentLoaded", init);
}

Important 1: do not encode the @name value in quotes (") since the Userscripts extension for Safari will have a syntax error while injecting scripts whose names contain quotes.

Important 2: you must think about iframes carefully (if you use them). For example, in Safari, you must invoke init() within iframes without waiting for DOMContentLoaded (see the code above, which should work in both Safari and Firefox). Besides, you (and your end users) must enable the userscript manager on all domains: the primary domain of your webpage and the domains of each iframe.

Notice that the script would be executed before loading the document content (DOM). Thus, we register a DOMContentLoaded event handler to invoke the init function when the DOM is available.

This article was a gentle introduction to browser extensions. For further documentation, ask Google, Copilot, or ChatGPT.

Buy me a coffee:

--

--

Sergejs Kozlovičs
Sergejs Kozlovičs

Written by Sergejs Kozlovičs

Sergejs is an Associate Professor of Mathematics and has Ph.d. in Computer Science. He is the main developer of webAppOS.

No responses yet