A few years ago, Google announced Manifest v3 that caused quite a bit of controversy, because it hinders content blockers by restricting blocking requests while observing them and limiting the number of rules that can be applied.
The EFF even called it „deceitful and threatening„. In this blog post, I will share my thoughts about Manifest v3 because I have actually ported my main extensions to it. In short: Manifest v3 is good, but has bad parts that make the whole experience itself kinda bad.
Sorry that certain apostrophes („“) are wrong, but WordPress transforms them in some weird way.
declarativeNetRequest
I want to talk about the controversial „declarativeNetRequest“ API first. This is a brand new API that can be used to block or modify outgoing network requests. The blocking part of it actually replaces the „blocking“ parameter of the „webRequest“ API. From a technical standpoint, the new API is way better because it does not allow inspecting web requests, which improves security and overall browser performance.
It works like this: You specify a condition on when the rule should match and then an action that should happen. You can block requests, redirect them, modify HTTP headers and upgrade the scheme to HTTPS all without ever looking at it. While redirecting you can replace parts of the URL (like the host, port, path, query) which greatly simplifies this compared to Manifest v2 – you don’t have to write a single line of code!
I needed this while porting my extension „CNL Decryptor“ to Manifest v3. CNL Decryptor is an extension that catches Click’n’Load requests to the popular JDownloader which are „““encrypted“““ (everyone just copies the example that uses a fixed key though), decrypts the links and shows them all in a new window where they can be copied easily. Perfect for when you just need the links without opening up JDownloader and adding them! This works because JDownloader opens up a web server on port 9666. There are actually two parts to it: First, the URL „127.0.0.1:9666/jdcheck.js“ is requested, which just sets the variable „jdownloader“ to „true“, meaning that jDownloader is started. A lot of sites check this first, so it is essential! After that, „127.0.0.1:9666/flash/addcrypted2“ is called (or /add for plain C’n’L) with the pseudo-encrypted URLs and the encryption function „f()“ (more like the key, since as I said before, it’s hardcoded everywhere).
The declarativeNetRequest rule looks like this:
{ "id": 1, "condition": { "regexFilter": "http://127\\.0\\.0\\.1:9666/jdcheck\\.js(?:\\?.*)?", "resourceTypes": ["script"] }, "action": { "type": "redirect", "redirect": { "extensionPath": "/web_accessible_resources/jdcheck.js" } } }
As you can see, I’ve used a simple regex filter to check for a request to „jdcheck.js“ (some sites append some query params, for whatever reason) and redirect it to an internal javascript file provided by my extension that just returns what JDownloader would return – it simply sets „jdownloader“ to „true“, fooling the site into thinking JDownloader is running.
In Manifest v2 I had to add an onBeforeRequestListener through the webRequest API and check the port manually, since it doesn’t support ports.
We have redirected the check now and the site can send the Click’n’Load links. I used this rule:
{ "id": 2, "condition": { "regexFilter": "http://127\\.0\\.0\\.1:9666/flash/add(?:crypted2)?", "resourceTypes": ["main_frame", "script"] }, "action": { "type": "block" } }
Well this… actually just blocks the request so that it doesn’t get send to JDownloader. Remember – as I said before – declarativeNetRequest can’t inspect contents of web requests so I still had to use it there. The rest of the code didn’t change though.
The extension currently won’t function with Manifest v3 though, because observable webRequests just stop listening due to a bug in Chrome from November 2019 (!). We will get to this shortly. This issue has been fixed in September 2022, shipping in Chrome 107.
You see one bad part of declarativeNetRequest right above – you can’t block network requests depending on the contents of the request. This alone won’t limit content blockers „too badly“ (I think it’s needed for dynamic rules and uMatrix), the bigger issue is the limited number of rules you can apply. There are two types of rulesets: static and dynamic.
Static rules are provided by the extension while installing and these are limited to GUARANTEED_MINIMUM_STATIC_RULES which is currently at 30000 (you can activate more, but they may not be activated because they will count towards the global browser rule limit which is shared between extensions and are „first-come, first-served“). So if e.g. uBlock Origin decides to bundle the EasyList with the extension, this wouldn’t work since it has over 55000 rules at the time of writing – excluding other lists like EasyPrivacy, Fanboy’s Annoyance List, regional EasyList filters and so on. And for every rule update, a new uBlock Origin version needs to be rolled out making this unfeasible.
But there are still dynamic rules that can be added at runtime and persist restarts and updates. These are limited to MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES though, which is currently at merely 5000. Oh and RegEx filters are limited to MAX_NUMBER_OF_REGEX_RULES which is at 1000 at the time of writing.
UPDATE: There is now uBlock Lite, demonstrating that it’s possible to dramatically lower the number of filters. Also, AdGuard ported their adblocker with a cosmetic blocker included.
Service Workers
WebExtensions were always kinda strange. They are like web pages running permanently in the background, but not really because they don’t have access to every web API and of course they have no DOM or something. This is a concept called „Event Pages“ and as a web developer, it was always weird to work with them.
Manifest v3 replaces them with Service Workers that are well known, an established technology and a web standard. Normal web pages can already use them for push notifications, caching things for offline use, doing stuff in the background and so on. They are a perfect fit for browser extensions and have a defined lifecycle. They run on a seperate thread so they don’t block the browser which is very important for performance.
Service Workers are JavaScript Workers, so they can’t access the DOM directly. They have to communicate through a content script and the „tabs.sendMessage“ API which makes this more verbose and difficult, but I have not needed this feature for now so I can’t say more about it.
Service Workers will be suspended after a certain time so the extension won’t run anymore – this is not a problem because you have to register EventListeners anyway but this may need a full rewrite for some extensions that depend on variables instead of using the Storage API. I actually did this because I was using an „init()“ function which first loads all settings and stores them in an object easily available to other functions. I had to rewrite it to always use the Storage API when e.g. an alarm is run.
Speaking of „init()“ functions: Don’t use them, you have to register EventListeners on the first run of the Event Loop, meaning in simple terms: don’t throw them inside some function or register them asynchronously – this won’t work! Just register them at the „top level“ of your Service Worker. You have to remember, that your Service Worker is executed every time it is run, unlike Event Pages. Use „chrome.runtime.onInstalled“ (check if reason === chrome.runtime.OnInstalledReason.INSTALL though) to set some initial settings and the action button plus context menus together with „chrome.runtime.onStartup„.
Service Workers are a nice idea, but you have to be careful, especially when you migrate an extension.
Oh and you can load Service Workers as an ES module, which is a nice bonus. Plus, every API is promisified, so goodbye callback-hell!
Host Permissions
This issue has been resolved on 5th April 2022.
Original Text:
Host permissions can no longer be added to „optional_permissions„. You see, you can add permissions as optional and ask for them at runtime, kinda like Android or iOS ask you about using the camera only when needing it. In Manifest v2, you can add host permissions to this, but it’s no longer possible.
My extension „FreshRSS Checker“ uses this for allowing access to just the domain that your FreshRSS instance is running to give you the number of unread articles. In the Manifest v3 port, the extension has access to all domains by default so I actually had to remove a security feature. Well, I planned to but I quickly noticed that optional host permission still work, but in a weird way.
In the extension setting, you have this dropdown „Site access“ which no one ever uses or notices. If you change „On all sites“ to „On specific sites“ and add some URL (what a horrible UX) you get the same behaviour as in Manifest v2. I guess this will be the default setting going forward because Google writes: „Moving forward, we’ll be changing host permissions to be optional by default, with explicit user consent required to grant site access“.
With the current implementation, this kinda defeats the security point of Manifest v3 until Google changes it.
Documentation
The extension documentation is a mess of Manifest v2 and v3 stuff and it doesn’t specify which article applies to which Manifest (only a few like the one describing background scripts). This will get better though, because Manifest v2 will be removed soon…
Removal of Manifest v2
UPDATE: Google moved the end of MV2 to June 2023/January 2024.
With these issues, Manifest v3 extension will be rough and a lot of extensions will be killed. If you enable dev mode in chrome://extensions and look which one uses Service Workers, you most definitely won’t find anything. And the removal of Manifest v2 is just one year away – January 2023. In January 2022, no new Manifest v2 extensions can be added and Firefox still does not support Manifest v3, which makes developing cross-browser extensions very hard.
The new features are great though, but it’s lame how Google wants to push this without fixing the biggest issues (or in the case of content blockers we can safely say that they ignore them).
I read in a tech press article somewhere that Vivaldi will not implement Manifest v3 the same as does Brave & Firefox. Is this publicly announced yet regarding Vivaldi refusing Manifest v3? I support the decision to refuse it.
Every Chromium-based browser will have to implement it, if they want or not. Else this would create huge divide in the codebase that a small team can not handle. Manifest v3 itself is not the problem as stated in the post here, the only worrying thing is the removal of blocking webRequest – this is something, that Vivaldi and Brave can choose to still support without too much hassle (I think). This is something that Firefox will be doing.
Very interesting reading, thank you for sharing it [even if much of it is technically beyond my experience]. Whilst i note your Dev-insight awareness that v3 holds certain benefits for some extension developers, i’m afraid that my focus on it now, the same as from the very first public mentions of v3, rests almost entirely on the apparently devastating ill-effect it will have on @gorhill’s two primary extensions, *uMatrix* & *uBlock Origin*. The former is [for quite a while now] an archived project, hence logically won’t receive any work by @gorhill to try to get around v3. Otoh, uBO is of course an active project, with a large user-base, & iirc i have read nothing but bad things from @gorhill about the lack of viability for uBO to continue, if v3 occurs & v2 goes. From my perspective as a uM / uBO user with huge numbers of accumulated **dynamic rules** [ie, i do not rely merely on the static lists], the putative death of these extensions would signal an unavoidable removal of all chromium-based browsers from my PCs, as the idea of using the internet *sans-uBO’s* enormous contribution to my privacy is simply untenable. In that light, in my selfish eyes, the advent of v3 is an unmitigated digital disaster, notwithstanding even if it does also have some new benefits. Simply put, if Mv3 kills uBO, then i despise it… & gargle.
I’d really like to see full PWA support on mobile version. While I have no issue on the desktop version, my PWAs aren’t working as expected on mobile(Add to homescreen isn’t launching automatically as on other Chromium based browsers).
If we’re going to talk about Service Worker and Manifest, please support them fully for PWA, even if not supporting all capabilities of the latest Manifest version.