Your engineering team runs code review, CI, branch protection and a deployment pipeline. Your marketing team can execute arbitrary JavaScript on every page of your site, including the checkout, by clicking publish in a web interface.
Both statements are true at most companies, and the second one is the tag manager. It is a loader whose entire job is to fetch and run third-party scripts chosen by people outside engineering, and it sits inside every control you built.
This post is the exposure and the controls that fit without taking the tool away from the people who need it. For whoever owns frontend security.
What the tool actually does
A tag manager is a small script on your pages that, at runtime, fetches a configuration and executes whatever scripts that configuration names. Marketing adds a tag, publishes, and within minutes every visitor's browser is running new code.
Three properties follow, and each defeats a different control:
No code review. The script comes from a vendor. Nobody at your company reads it before or after.
No deployment pipeline. Publishing bypasses CI, staging and your release process entirely. There is no artifact, no commit and no rollback in your systems.
The code can change after approval. A vendor tag is usually a loader too: it fetches the real payload from the vendor's CDN at runtime. You approved the tag once; the code it runs today is whatever the vendor serves today.
That last one is the important one. Reviewing a third-party tag tells you what it did on the day you looked.
Why this is where card skimming happens
Client-side attacks target this path because it is the shortest route to executing code on a page where users type sensitive data.
The pattern is consistent: compromise a script that many sites load, or the CDN serving it, and the malicious version reaches every site that trusts it. The polyfill.io case is the widely known example of a trusted script's ownership changing and the served content changing with it. The same shape applies to any tag that loads from a third party.
A payment page running a tag manager is running code from a vendor, which loads code from that vendor's CDN, which may include code from a fourth party. Your users type card details into a page with that chain on it.
The controls that work
Keep it off the pages that matter. The strongest control and the least popular. No tag manager, no third-party scripts, nothing but your own code on the checkout, the login page and anything handling credentials. Marketing loses analytics on those pages, which is a real cost and a defensible trade.
If you cannot remove it, isolate the sensitive fields into an iframe served from a different origin, so scripts on the parent page cannot read them. This is what payment providers' hosted fields do, and it is why they exist.
Content Security Policy, meaningfully. A CSP that allowlists the tag manager's domain and permits unsafe-inline grants exactly what an attacker needs. To be worth anything it has to restrict which origins scripts may load from, and that constrains what marketing can add, which is the point and also the reason these policies get relaxed.
Run it in report-only mode first and read the reports. You will discover scripts you did not know were loading.
Subresource integrity where the script is versioned. A hash pinning the exact content:
<script src="https://cdn.vendor.com/lib.v2.4.1.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+..."
crossorigin="anonymous"></script>
The limitation is honest: SRI cannot protect a script the vendor intends to update at a stable URL, which is most tag payloads. It works for pinned library versions and not for the dynamic case.
An approval workflow inside the tag manager. Most support publish permissions separate from edit permissions. Turn that on, so adding a tag requires a second person, and make one of them someone who will ask what the script does.
Monitor what actually loads. The only control that catches a script changing after approval. Periodically fetch your pages, enumerate the script origins, and diff against an expected list. A new origin appearing is the signal, and it is the one thing that detects a compromised vendor.
If you take card payments
PCI DSS 4.0 addresses this directly. Requirement 6.4.3 covers managing and authorising the scripts on payment pages, and 11.6.1 covers detecting unauthorised changes to those pages. These became mandatory in 2025, and the reason they exist is exactly the attack described above.
Practically that means an inventory of every script on the payment page with a justification, a mechanism ensuring only authorised scripts run, and change detection. If your payment page loads a tag manager, satisfying these is difficult, which is a strong argument for the isolation approach.
Audit yours this week
Open your site and enumerate what loads:
# every external script origin on a page
curl -s https://yoursite.example.com/checkout \
| grep -oE 'src="https?://[^"]+"' | cut -d/ -f3 | sort -u
That only catches scripts in the initial HTML. The tag manager loads more at runtime, so also open the page in a browser, look at the network panel, and list every domain. Compare that list to what you expected.
The usual result is more domains than anyone can account for, several added years ago for campaigns that ended, each one a party that can execute code in your users' browsers.
The concession
Marketing needs these tools, and the reason tag managers exist is that requiring an engineering deploy for every tracking change was genuinely untenable. A security position that amounts to removing the capability will lose, and should, because the business need is real.
The workable split is by page. Sensitive pages get nothing. Everything else gets the tag manager with CSP, an approval workflow and monitoring. That preserves the capability where it matters commercially and removes it where the consequences are worst, and it is a conversation about which pages rather than about whether marketing may have tools.
The implication
You have two deployment pipelines. One has review, testing and rollback. The other has a publish button and no engineer in the path, and it reaches the same browsers.
Nobody designed it that way. It is worth at least knowing which pages the second one runs on.