Third-Party Risk

Obfuscation, tag managers and the scripts you do not control

There is a failure mode in protected applications that looks like a bug in the protection and is not. The tamper guard fires on real visitors, intermittently, with no attacker involved — because an analytics tag wrapped the same API the guard was watching. It is worth understanding precisely, because the fix is a one-line exclusion and the wrong fix is turning off a control you wanted.

Your protection stops at your bundle

A tag manager is a hole you deliberately cut in your own deployment process. Its entire value is that somebody who is not on the engineering team can add JavaScript to a production page without a release. That is genuinely useful, and it means the page your visitor loads is assembled from code that never passed through your build.

So the boundary is simple and worth saying out loud: protection applies to what you protect. Your bundle can be renamed, string-encrypted, integrity-checked and watermarked, and the analytics tag loaded next to it is still whatever the vendor shipped this morning. The two run in the same document, share the same globals, and have the same access to the DOM and to anything a user types.

This is also why Subresource Integrity helps less here than people hope. SRI pins a known hash for a script you reference. Tag managers exist to load scripts you did not reference, decided at runtime by a configuration living in someone else console.

The collision nobody plans for

Here is the concrete failure, and it is common enough to be worth checking before you ship. AntiMonkeyPatching watches a set of globals and reports the first replacement it sees. The default watch list is specific:

  • fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon
  • EventTarget.prototype.addEventListener, setTimeout, clearTimeout, Promise.prototype.then
  • Storage.prototype.getItem and setItem, crypto.subtle.digest
  • JSON.parse and stringify, several Array.prototype methods, Object.defineProperty, Function, Function.prototype.toString and eval

Now read that list as an analytics vendor would. Instrumenting outbound requests means wrapping fetch, XMLHttpRequest and sendBeacon. Capturing interactions means wrapping addEventListener. Session replay wraps timers and storage. Error monitoring wraps Promise.prototype.then to catch unhandled rejections. The default watch list and the standard analytics instrumentation surface are very nearly the same list, because both target the places where an application talks to the outside world.

The result is a tamper signal fired by a tag your own marketing team installed, on a schedule set by whenever that tag loads relative to your bundle. If your failure action is anything harsh, you have built a self-inflicted outage with an intermittent trigger.

Fixing it without turning the guard off

The instinct is to disable AntiMonkeyPatching. Do not; it is the wrong granularity for a problem that is precisely enumerable.

AntiMonkeyPatchingExcludeGlobals takes the specific dotted paths to stop watching. If your analytics vendor wraps fetch and sendBeacon, exclude those two and keep the other twenty under observation. You lose visibility into two APIs you had already decided to let a third party own, and you keep the guard meaningful everywhere else.

Two practical notes. The clean-realm comparison changes which patches you can see at all: it compares against a pristine realm rather than against a snapshot your own code took, so a tag that loaded before your bundle is still detectable. That is usually what you want, and it also means a synchronous tag at the top of <head> will now be caught where a naive snapshot would have recorded its version as normal. And choose the failure action deliberately: callback and degrade are the defensible ones for anything a third-party tag can trigger. Blank and throw convert a marketing deployment into a site outage.

What actually watches the tags

Excluding globals stops the false positives; it does nothing about the real risk, which is a tag that turns hostile. That is what the third-party script inventory is for, and it was built for this threat specifically — the Magecart pattern, where an attacker compromises one CDN, marketing script or tag manager and injects a skimmer into a page that is otherwise sound.

It works on four signals. It snapshots every <script> present at DOMContentLoaded. It intercepts document.createElement("script") so DOM-injected scripts are recorded at creation, even if the injector removes the element afterwards. It uses a PerformanceObserver on resource entries to catch loads that bypassed createElement entirely, such as iframe-injected scripts or worker importScripts. And it compares everything observed against an allowlist of script origins and inline content hashes, beaconing anything outside it.

Two of its detections are worth calling out because they encode real attacker behaviour rather than generic policy. A script injected after document.readyState reaches complete is flagged, because deferred injection is a standard evasion for scanners that check the page once at load. And a script whose URL is on the allowlist but whose content hash changed between page loads is flagged too, which is the supply-chain swap case where the CDN entry you approved is quietly serving something else.

What obfuscation does and does not change for analytics

A reassurance and a warning, in that order.

The reassurance: session replay, heatmaps and product analytics keep working. Those tools key off the DOM — element structure, class names, IDs, data attributes, text content — and obfuscation does not touch markup. Your selectors, your funnels and your replay sessions survive protection unchanged. The same property that makes obfuscation useless against a DOM-only scraper makes it harmless to DOM-based analytics.

The warning: error monitoring does not come through free. Stack traces from protected code arrive with renamed frames, and they are only readable if you kept the protection report for that exact build and run them back through symbolication. Lose the report and the traces are permanently unreadable, which teams usually discover during an incident rather than before one.

A short checklist

  • Inventory what your tag manager can actually load, not what it loads today. The container configuration is the real deployment surface.
  • Enumerate which globals your tags wrap, and put exactly those in AntiMonkeyPatchingExcludeGlobals.
  • Keep the failure action at callback or degrade for any signal a third-party tag can plausibly trigger.
  • Run the script inventory with a real allowlist of origins and inline hashes, and route violations somewhere a human reads.
  • Archive a protection report per release so error-monitoring traces stay symbolicatable.
  • Remember the tag manager itself is a credentialled write path into production. Its access controls deserve the same scrutiny as your deploy pipeline.

The short version

Protecting your bundle says nothing about the code loaded beside it, and the two interact in a way that surprises teams: the APIs analytics vendors wrap are almost exactly the APIs the anti-tamper guard watches, so your own marketing tags will trip it. Exclude the specific globals rather than disabling the guard, keep the failure action proportionate, and use the script inventory with an origin and hash allowlist to watch what the tag manager brings in. DOM-based analytics is unaffected by protection; error monitoring is, and only a retained protection report keeps it readable.

Frequently asked questions

Does obfuscating my bundle protect the scripts loaded by my tag manager?

No. Protection applies to the code you put through it. A tag manager exists to load JavaScript decided at runtime by a configuration outside your build, so those scripts arrive exactly as the vendor shipped them, share your globals, and have the same DOM access your protected code has. Subresource Integrity helps less than expected here too, because it pins hashes for scripts you reference and a tag manager loads scripts you did not reference.

Why does my analytics tag trigger the anti-tamper guard?

Because the watch list and the standard analytics instrumentation surface are nearly the same set of APIs. The default watched globals include fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon, EventTarget.prototype.addEventListener, setTimeout, clearTimeout, Promise.prototype.then, Storage getItem and setItem, and crypto.subtle.digest. Instrumenting outbound requests, capturing interactions, replaying sessions and catching unhandled rejections means wrapping exactly those, so a legitimate tag reads as a replacement and fires the signal.

How do I stop false tamper alarms without disabling the guard?

Use AntiMonkeyPatchingExcludeGlobals with the specific dotted API paths your tags wrap. If the vendor wraps fetch and sendBeacon, exclude those two and leave the other watched globals under observation. Disabling the whole feature is the wrong granularity for a problem you can enumerate exactly. Pair it with a proportionate failure action, since callback and degrade let you record the signal without turning a marketing deployment into an outage.

Does obfuscation break session replay, heatmaps or product analytics?

No. Those tools key off the DOM, meaning element structure, class names, IDs, data attributes and text content, and obfuscation does not change markup. Selectors, funnels and replay sessions all survive protection unchanged. Error monitoring is the exception that does need work, because stack traces come back with renamed frames and are only readable if you kept the protection report for that build and run them through symbolication.

What does the third-party script inventory actually detect?

It snapshots every script element present at DOMContentLoaded, intercepts document.createElement for script so DOM-injected scripts are recorded at creation even if the element is removed afterwards, and observes PerformanceObserver resource entries to catch loads that bypassed createElement such as iframe-injected scripts or worker importScripts. Everything observed is compared against an allowlist of origins and inline content hashes, and anything outside it is beaconed with the full inventory snapshot.

Can it catch a compromised script that was already on my allowlist?

That is one of its specific signals. It flags a script whose URL is allowlisted but whose content hash changed between page loads, which is the supply-chain swap case where an approved CDN entry starts serving something else. It also flags any script injected after document.readyState reaches complete, because deferred injection is a common evasion against scanners that inspect the page once at load time.

Related reading