Background
I've run into a website which has advertisement banners on it. When an adblock or equivalent removes the banner elements, the website falls back to spamming alerts
about being unable to load advertisements, making the viewing experience worse than with the ads.
I like to tinker around and try to break "protections" like these on websites, however, at this time I am out of ideas.
The website in question seems to employ extensive protections against tampering in general, so while this question is "simply" about doing something with the object freezing done in the head, there are multiple limitations as to how this can be done.
The problem
First of all, I don't want to solve this by doing something special to the banners, such as making them visible instead of removing them. I'd like to attack the alert
.
The problem is that when the website loads its index.html
, it freezes the window.alert
object in the <head>
section like so:
<script>
Object.defineProperty(window, 'alert', {
configurable: false,
writable: false
})
</script>
After which it is impossible to do, e.g.:
window.alert = null;
Given that we can't touch the alert itself, one might think to try to find and remove the call to the alert alternatively...
...Unfortunately this does not seem to be possible either, because the website will not work without JavaScript and the website loads the rest of its scripts with a CSRF-token. All of the loaded scripts also utilize the CSRF-token in every request.
The website loads one "loader-script" initially in the head and it is protected by origin policies, so this script cannot be served from anywhere else. This loader-script itself uses a CSRF-token to load rest of the scripts with XMLHttpRequests.
There are also content-security-policy -headers in place.
This essentially means that it is not possible to, e.g. have a web server acting as a proxy, which would load the web page, strip the freezing script in the head and pass the modified web page over. At least to my knowledge that is - as far as I know, this is exactly what CSRF-token prevents.
Where I'm at
I am using a chrome extension called Resource Override, which lets me, for example:
- Set regex-based rules and actions for websites
- Further set regex-based URL-rules and actions for when website request some specific URL
- "respond" to any request a website makes and respond with, e.g. a locally served file, with the limitation that this this locally served file will obviously fail with everything that requires a CSRF-token
- Inject JavaScript anywhere on the website, so long as the website is loaded
- Modify request and response headers
Injecting js in the head
The obvious first move for me was to simply inject a window.alert = null
in to the <head>
of the website, but this did not work as when the page and the html of the page loads, the object freezing already exists in the head.
Furthermore, this method can never work, because one can only inject scripts into the page when there is a page to inject the script in, but in this case when such page exists, the object freeze code is already there as well.
Unfreezing / thawing the window.alert object
There are many Stackoverflow questions and answers regarding unfreezing / thawing frozen objects in JavaScript. In short, it is not possible. At least not in a way that would help here.
Prototype pollution
To be honest, I am not too familiar with the applications of prototype pollution attacks, but I did look a bit into them because I felt that fiddling around with the __proto__
could somehow allow me to make the window.alert
configurable or writable. It seems like this is not the way either though.
One thing I tried out with the knowledge I have was to simply extend the Object.prototype
, which is sort of a "root prototype" for all objects, with a property named alert, so:
Object.prototype.alert = 999;
What this does is that when you now do something like this:
const arr = [1, 2, 3];
const str = "abc";
for(prop in arr) {
console.log(prop) // will log alert at some point
}
console.log(str.alert) // will log 999
On an interesting side note, the only way to properly get ALL of the enumerable property names of an object, including the ones in the prototype chain (the alert), seems to be to use a for...in
-loop as shown. All other methods, such as the conveniently named Object.getOwnPropertyNames
, will only return the properties directly on the object, but not it its prototype chain. Documentation for this can be found here.
Anyway the idea with this was to:
A) Get lucky and somehow overwrite window.alert as well, since window is also an object and therefore should get extended by Object.prototype.x
B) Get lucky and hope that the website calls window.alert
in some ridiculous manner, so that having the alert-property on everything would mess this up
Unsurprisingly, neither of these happened.
Proxy objects
I also played around with Proxies with the initial understanding that it would let me sort of "hook" into function calls, so that I could intercept the call to the alert-function itself. This is not how Proxies work though, so lesson learned.
It did raise the question though, if it's possible to intercept / listen to function calls in JavaScript some other way. I can obviously wrap any function with a function of my own, but then everything would need to call the wrapping function instead of the original one, which doesn't help here.
Reflect API
There is also a JavaScript equivalent of a "Reflection API", but by the looks of it and some tests I did, it also doesn't offer anything with regards to hooking / listening / intercepting / redirecting function calls, though the documentation somewhy states that:
Reflect is a built-in object that provides methods for interceptable JavaScript operations.
Local overrides with Chrome
Scripts modified by Chrome local overrides do not pass some kind of integrity check and therefore they will be blocked.
Other ideas
As per usual with these things, I fiddled around with various different miscellaneous ideas such as:
- Tried to replace the whole window object in the hopes of "unlocking" window.alert
- Naively tried to just set window.alert configurable and writable again
- Tried
Object.freeze = null
, though it would have already been called by the time it could be set null or modified anyway
Now what
Anything is possible in my opinion, though it could be that the solution in this case might be complicated. I think I have exhausted most of the quick and easy ways, but I'm hopeful that I've missed something.
One idea that is still in-progress for me is to first of all try to intercept and strip the content-security-policy headers to better allow intercepting and modifying requests which fetch scripts. I'm not sure if this will work in the end either, because it seems like there is also some kind of integrity validation going on.
Interestingly enough, there does not seem to be a policy in place for inline scripts, so those do work. It is because of this why I am interested in JavaScript-based solutions the most, but I'll take anything.
I'd also be very interested to know if there is a way to run JavaScript or kind of setup a "modified JavaScript context" before even loading a website? I guess one could go tinker with V8 and delete the whole alert-functionality at its core, but I am looking for something little less than that.
Recap
- Website is spamming
window.alert
- Website freezes the alert object, making it unmodifiable
- Website requires JavaScript to work
- Website uses a CSRF-token extensively, even for loading scripts in the first place, so simply swapping files is not possible
- Website loads all the important scripts with XMLHttpRequests and does so from within a CSRF-token protected script, which itself is protected by origin policies
- Website utilizes content-security-policy -headers
- Script files have integrity checks in place, so e.g. chrome local overrides modifications cannot be used
- The goal is to "simply" disable such website from making alerts
Final words
I wouldn't be surprised for this to just get nuked due to the broadness of it for one, but at least I had fun writing it. I find it really interesting how seemingly well a website, as well as specific functionality of it, such as the alert-function, can be secured against tampering these days - investigating this has been a eye opener for me.
Even so, It is quite a bit ridiculous how many different measures need to be used; origin checks, integrity checks, hashed inline scripts, content-security-policies, CSRF-tokens, scripts loaded in very specific ways and in very specific places, ... All to prevent me from setting one little alert function to null in a sense!
If there is one takeaway from all this, that would be the ENORMOUS QUESTION of why in the everliving f- is it not possible to simply block websites from sending alerts? There are permissions controls for all kinds of features, so how come alerts and prompts are not one of them?
Edit
Like I said in my own answer, it turned out that the website had even more extensive anti-tampering measures than I expected and therefore I am including here what I dug up after the fact. Even if one manages to make the alert object writable, it still can't be replaced so easily, as that would trigger additional explicit anti-tampering checks.
In my answer, I will go through bypassing all of these checks, but here's what we're dealing with:
tamperCheck()
{
for (const check of [
{ob: Function.prototype, fun: 'toString'},
{ob: window, fun: 'alert'},
{ob: window, fun: 'confirm'},
{ob: Object, fun: 'freeze'},
{ob: Object, fun: 'seal'},
{ob: Object, fun: 'getPrototypeOf'},
{ob: Object, fun: 'getOwnPropertyDescriptors'},
]) {
const regex = new RegExp('^function ' + check.fun + '\\(\\)\\s*{\\s*\\[native code]\\s*}$');
let d;
try {
d = delete check.ob[check.fun]['toString'];
} catch {
d = false;
}
if (
!d
|| typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'
|| typeof check.ob[check.fun] !== 'function'
|| check.ob[check.fun].name !== check.fun
|| check.ob[check.fun].toString().replaceAll('\n', '').match(regex) === null
) {
return true;
}
Object.defineProperty(check.ob, check.fun, {value: check.ob[check.fun], configurable: false, writable: false});
}
return false;
}