Runtime Integrity
Published
The question usually arrives with a specific irritation attached: an auto-clicker beating your rate limits, a coupon extension inventing discounts, a userscript unlocking a feature, a scraper reading your prices. The short answer is no — and the useful answer is that this is one of the few places where polymorphic output does more work than any dedicated defense.
The privilege model is not in your favour
An extension is not a peer of your page. It is installed by the user, granted permissions by the user, and positioned by the browser above the page in the trust hierarchy. This is a design decision rather than an oversight: a user agent that could be overruled by the sites it visits would not be much of a user agent.
The practical consequence is a race you lose before it starts. A content script configured to run at document start executes before your bundle has been fetched, let alone parsed. In that window it can replace globals, install hooks on anything it likes, and leave your code running against an environment it prepared. Whatever defensive code you write runs second, in a world the extension already edited.
Notice how this differs from disabling devtools, which is a losing fight against a tool that is simply outside the page. Here the other party is running inside the same document, earlier, with more privilege. There is no arrangement of page JavaScript that changes that ordering.
Most of them are helping
This is the part that gets skipped, and skipping it produces genuinely harmful products.
Enumerate what is actually patching a typical page. Password managers, hooking form APIs to autofill. Accessibility overlays and screen-reader helpers, replacing event and timer APIs to observe changes. Translation tools rewriting text nodes. Corporate security agents installed by an employer, which the user cannot remove. Ad blockers, on a large fraction of the web. Observability and session-replay agents, sometimes ones your own company installed. Every one of those is indistinguishable, from inside the page, from a hostile userscript — because the technique is identical.
Some of that population depends on the patching for access rather than convenience, which is exactly the collision described in the accessibility article. A control that reacts harshly to a detected patch will, on volume, deny service mostly to people who did nothing wrong — while the adversarial minority disables the extension, reloads, and continues.
What obfuscation actually does here
Now the part that works, which is not the part people expect.
A userscript that targets application logic needs anchors. It has to find the function that submits the order, the flag that gates the feature, the object holding the cart. In readable or merely minified code those anchors are trivial to locate: a distinctive function name, a global, a string, a recognisable shape.
Obfuscation removes the readable anchors, and that is worth something on its own. But the far more effective property is one that is usually discussed as a build-hygiene detail: output is polymorphic per build by default. The same source protected twice produces different names and different structure. So the userscript author does not just do the analysis once — they redo it every time you ship. Their script breaks on your release cadence, their users file complaints, and maintaining it becomes a standing commitment rather than an afternoon.
That is attrition, and attrition is what actually clears out casual tampering. It is also the one case where you specifically do not want a fixed seed. Reproducible builds are valuable for audit and verification, and a seed makes protected output byte-identical across rebuilds — which also hands a script author stable anchors across releases. If userscript attrition is the goal, leave the seed off for the code paths you care about and get your reproducibility guarantees from the manifest hashes instead.
Two honest limits. A userscript that only manipulates the DOM is untouched, because obfuscation does not change your markup — your class names, IDs and element structure are as stable as they ever were. And any name you excluded from renaming to keep a public API working is, by definition, a stable anchor you handed over.
Detection, and what it is good for
AntiMonkeyPatching monitors a set of browser APIs — event registration, timers, Fetch, XHR, Promise, storage and Web Crypto — and reports the first replacement it observes through your configured runtime-defense action, re-checking on a heartbeat rather than only at load.
The pre-load problem is real and is what the clean-realm option addresses. A snapshot taken by your code after an extension has already patched something records the patched version as normal. Comparing against a pristine realm instead of against your own snapshot catches the patch that arrived first. Where the patching is expected — your own observability agent, an accessibility tool you support — AntiMonkeyPatchingExcludeGlobals takes the specific dotted API paths, which is a much better instrument than turning the feature off.
What it cannot see: anything that never touches a monitored API. A script that rewrites the DOM, reads your state and clicks buttons is invisible to API monitoring, and that describes most scrapers and auto-clickers. For those, the relevant discussion is in does obfuscation stop web scrapers, and the answer is again server-side.
Related but distinct, the environment-integrity checks compare watched builtins against their native shape and against a pristine realm, and look for automation markers. Note how their documentation grades findings: automation results are capped at signal severity and never reported as high, precisely because they are cheap to spoof. That is the right calibration for this entire category.
The response should be proportionate
The runtime-defense action set includes throw, blank, redirect, reload, callback and degrade. For extension detection, only the last two are defensible.
- Callback lets you record the signal with the session, feed a risk score, and decide later with more context than a single page load can offer.
- Degrade reduces the surface — require re-authentication for a sensitive action, disable a bulk operation, add a confirmation step — while leaving the user able to work.
- Blank, throw, redirect and reload are the wrong shape. A blank page for someone whose employer installed a security agent is an outage they cannot explain or fix, and your support team will not diagnose it either.
The general principle: use the signal to change what you trust, not to change whether the page renders. A tamper signal is excellent input to a server-side decision about whether this session may transfer funds. It is a poor basis for refusing to display a page.
Where the real fix lives
Every concrete complaint in the opening paragraph has a server-side answer, and none has a client-side one.
- Coupon and discount extensions. Validate codes, eligibility and final price on the server. If the client can compute the discount, the client can choose it.
- Auto-clickers and automation. Rate limits, quotas and abuse detection belong behind the API, where the request arrives regardless of what generated it.
- Feature-unlock scripts. Do not ship the feature and gate it in JavaScript. Entitlements are a server response; unentitled functionality should not be in the bundle at all.
- Scrapers. Server-side rate limiting and access control, as the scraping article covers.
- Rehosted copies of your app. This one does have a client-side control: domain locking, which is the primary mechanism described in the widget and SDK guide.
The short version
You cannot prevent an extension from modifying your page, because it runs earlier and with more privilege, and a large majority of the extensions doing it are serving your users rather than attacking them. What you can do is make targeted userscripts unprofitable to maintain — and per-build polymorphic output does that better than any detection feature, by breaking the attacker’s work on every release. Detect patches for telemetry and risk scoring, exclude the APIs your own tooling and your users’ accessibility software legitimately replace, respond by degrading rather than blocking, and put every decision that actually matters behind your API where the browser cannot vote on it.
Frequently asked questions
Can a website prevent a browser extension from modifying it?
Not reliably, because the extension is above the page in the browser’s privilege model rather than beside it. A content script configured to run at document start executes before your bundle has been parsed, which means it can replace globals, install hooks and rewrite the DOM while your first line is still waiting. Nothing you write inside the page can pre-empt code that ran earlier. The realistic goal is to detect that it happened and choose a response, not to prevent it.
Does obfuscation stop userscripts targeting my site?
It makes them expensive to write and fragile to maintain, which in practice is most of the battle. A userscript that manipulates your application logic needs stable anchors — a function name, a global, a recognisable code pattern to hook. Renaming removes the readable anchors, and per-build polymorphic output means the anchors that remain move on every release. The script breaks each time you ship and someone has to redo the analysis. A userscript that only manipulates the DOM is unaffected, because obfuscation does not change your markup.
What does AntiMonkeyPatching detect and what does it miss?
It watches a set of browser APIs including event registration, timers, Fetch, XHR, Promise, storage and Web Crypto, and reports the first replacement it observes through your configured failure action, re-checking on a heartbeat rather than only at startup. Patches applied before your bundle loads are the harder case, which is what the clean-realm comparison exists for — it diffs the current environment against a pristine one instead of against a snapshot your code took after the fact. What it misses is anything that never touches a monitored API, most obviously a script that only rewrites the DOM.
Should I block users who have extensions that patch my page?
No. The overwhelming majority of patched environments belong to people doing nothing wrong: password managers, accessibility overlays, screen readers, translation tools, corporate security agents installed by an employer, and ad blockers. Several of those support users who are legally entitled to the accommodation. Blocking on a tamper signal denies service to that whole population in order to inconvenience a much smaller group who can simply disable the extension and reload. Log it, attribute it, and degrade if you must.
How do I stop a coupon or price-scraping extension from abusing my checkout?
By validating on the server, which is the only place the answer can be trusted. If a discount code is checked in client-side JavaScript then an extension — or a devtools console, or a request replayed by hand — can make that check succeed. Move validation, eligibility and price calculation behind your API and let the client render a result it was given. Obfuscation raises the cost of understanding your checkout logic, which is worth having, but it cannot make a decision computed in the browser authoritative.
Does a Content Security Policy keep extensions out?
No. Extension content scripts are not subject to your page’s CSP in the way page scripts are, and browsers deliberately preserve that behaviour so user agents remain agents of the user. CSP is genuinely valuable against injected page-level scripts, third-party tag sprawl and exfiltration destinations, and it is worth configuring well for those reasons. It is not an extension control, and a policy written on the assumption that it is will produce a false sense of coverage.
Related reading