(was going to post this in security, but seems appro here, with MV2 deprecated)
(...)
But in the old days, Google decided it'd be a good idea to inject a bunch of JS files into pages that used Chrome APIs. These "extension binding modules" would initialize API functions and validate arguments before passing them to the browser.
Turns out running privileged JavaScript in user-controlled websites was not a good idea, because JS can often be manipulated by overriding global functions and prototypes. Since certain APIs like chrome.runtime exist on normal websites too, the extension bindings system led to multiple Universal XSS bugs back in 2015 and 2016. Here's one that allows any website to inject code into any other website. Truly crazy stuff. If only I weren't 8 years old back then... maybe I could have cashed in.
Anyway, Google learned from their mistake and moved most API bindings to pure C++. However, a couple of JS binding files still exist and are used today. For example, if a Chrome extension runs the following code, it'll hit a JS loop and hang infinitely: (as of July 2025)
chrome.permissions.contains({ permissions: { length: Infinity }})
Maybe you are wondering what this has to do with adblockers.
Remember how I said only a few APIs still use JavaScript bindings? chrome.webRequest is one of them.
The bug
This is how an MV2 extension would block requests to example.com:
chrome.webRequest.onBeforeRequest.addListener(() => {
return { cancel: true }
}, { urls: ['*://*.example.com/*'] }, ['blocking'])
It's the 'blocking' part at the end that requires the webRequestBlocking permission, and therefore isn't allowed in MV3. Without it, the cancel: true does nothing.
So clearly adding a blocking listener to the chrome.webRequest.onBeforeRequest event does not work anymore. But we can do something crazy. We can make our own event. Now, this should not be possible; it's not even a concept that makes sense. But, because of how the JS bindings work, you can do it. For some reason, there is a wrapper class for webRequest events that contains some extra state.
(A note on the security of the above code.)
Instead of doing pure bindings between JS and C++, the browser creates one of these classes for every chrome.webRequest event: onBeforeRequest, onCompleted, etc. Surprisingly, the .constructor of these events is still public. It points to yet another wrapper class, which internally calls WebRequestEventImpl (from the code above). You can use this to can create a new event with your own properties:
let WebRequestEvent = chrome.webRequest.onBeforeRequest.constructor
let fooEvent = new WebRequestEvent("foo")
There is still a lot of validation going on in the backend when you try to actually do things with these fake events. For example, trying to add a listener to fooEvent kills the extension's process, because the event name is invalid. So how do you manipulate the properties of WebRequestEventImpl to do anything interesting?
After a lot of time looking into the C++ code, I found exactly one vulnerable thing: the opt_webViewInstanceId parameter. This was set for Chrome platform apps, in order to let them manage their embedded websites (WebViews). Among other things, it let them use web request blocking to control navigation. Basically, if an event had a WebView ID, the permission check for webRequestBlocking would be skipped. The issue was that the browser never verified that an event with a WebView ID actually belonged to a platform app. So an extension could spoof it, skip the check, and use the blocking feature.
let WebRequestEvent = chrome.webRequest.onBeforeRequest.constructor
// opt_webViewInstanceId is the 5th argument
let fakeEvent = new WebRequestEvent("webRequest.onBeforeRequest", 0, 0, 0, 1337)
fakeEvent.addListener(() => {
return { cancel: true }
}, { urls: ['*://*.example.com/*'] }, ['blocking'])
Maybe I should note that platform apps were deprecated in 2020. I found this bug in 2023, and the code to handle opt_webViewInstanceId still exists in 2025. Goes to show how ancient code leads to bugs.
What could have happened, and what happened
Technically, someone could have used this bug to make a perfectly working adblocker in MV3 by simply replacing all instances of chrome.webRequest.onBeforeRequest with fakeEvent. This would have been very funny, after all the hype about how adblockers were being killed.
But I don't know how to make an adblocker, so I decided to report the issue to Google in August 2023. It was patched in Chrome 118 by checking whether extensions using opt_webViewInstanceId actually had WebView permissions. For the report, I netted a massive reward of $0. They decided it wasn't a security issue, and honestly, I agree, because it didn't give extensions access to data they didn't already have.
https://0x44.xyz/blog/web-request-blocking/
Chrome and Javascript bindings for extension APIs (split from t=27294)
Chrome and Javascript bindings for extension APIs (split from t=27294)
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Re: Chrome and Javascript bindings for extension APIs (split from t=27294)
Umm... except that the thread you posted in isn't discussing Chrome's deprecation of MV2 at all. This belongs in Web Tech. Split to a new topic there.
*Always* check the changelogs BEFORE updating that important software!
Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0