Educational Purpose Only. This article and Lab 2 are strictly for educational purposes. Code examples demonstrate attack techniques to help security professionals understand and defend against them. Never use these techniques on systems you don’t own.
Beyond the Submit Button
Classic Magecart attacks (covered in Lab 1) wait for a user to hit “Submit” before stealing payment data. DOM-based skimming is more aggressive: it captures data keystroke by keystroke, in real time, before any form is submitted.
The attacker does not need to intercept a form submission event. By the time the victim clicks “Pay,” their card number, CVV, and billing details are already sitting on a remote C2 server.
Lab 2 demonstrates three progressively sophisticated variants of this attack, all targeting a simulated online banking portal (SecureBank):
| Variant | Technique | Stealth Level |
|---|---|---|
| DOM Monitor | MutationObserver + event listeners | Medium |
| Form Overlay | Dynamic fake form injection | High |
| Shadow DOM Skimmer | Closed Shadow DOM isolation | Very High |
Real-World DOM-Based Attacks
DOM-based skimming has been observed in the wild across multiple threat actor groups:
| Attack | Group | Technique | Impact |
|---|---|---|---|
| Inter Skimmer (2019) | Magecart Group 12 | Real-time keystroke capture via DOM events | 1,500+ compromised stores |
| Pipka Skimmer (2019) | Unknown | Self-removing script post-execution | Targeted Shopify sites |
| ImageID Skimmer (2020) | Multiple groups | DOM mutation with obfuscated payloads | Eastern European targeting |
| Cockpit Skimmer (2021) | Magecart Group 8 | jQuery prototype pollution + DOM hooks | SaaS checkout platforms |
These attacks are harder to detect than classic Magecart because they:
- Leave no trace in form submission network requests
- Exfiltrate data continuously rather than in one POST
- Survive page navigation in single-page applications (SPAs)
- Use legitimate browser APIs in ways that are difficult to distinguish from normal behavior
The Target: SecureBank
Lab 2’s target is a simulated online banking portal with four forms containing high-value data:
SecureBank Dashboard
├── Add Card Form → Card number, cardholder name, expiry, CVV, billing zip
├── Transfer Form → From/to accounts, amount, memo
├── Bill Pay Form → Payee, account number, amount, date
└── Card Actions Modal → CVV verification
Unlike a one-page checkout, a banking portal presents a richer attack surface — multiple forms across tabs, dynamically loaded modals, and persistent sessions that make real-time exfiltration highly effective.
Variant 1: DOM Monitor (Real-Time Field Capture)
How It Works
The DOM Monitor attack uses three browser APIs in combination:
MutationObserver— watches the entire DOM for new forms and input fields as they appear- Event listeners — attaches
keydown,keyup,input,focus,blur, andpastehandlers to every targeted field setInterval— exfiltrates captured data to the C2 server every 5 seconds
The attack initializes with a targeted field selector list covering 16+ field types:
| |
The MutationObserver Setup
This is the core of what makes DOM-based attacks persist across dynamic UI changes — tab switches, modal popups, and SPA navigation:
| |
Why
subtree: true? Without it, the observer only watches direct children ofdocument. Settingsubtree: truewatches the entire DOM tree — ensuring newly rendered modals, dynamically injected checkout widgets, and lazy-loaded payment iframes all get picked up automatically.
Per-Field Event Monitoring
Once a field is discovered, the attacker attaches a full event listener suite:
| |
The isHighValueField() function identifies fields that warrant immediate exfiltration rather than waiting for the 5-second interval:
| |
What the C2 Server Receives
Every 5 seconds (or immediately for high-value fields), the skimmer sends a payload like this:
| |
Note that the attacker receives the card number being typed character by character. Even if the user notices something is wrong and clears the field, the data is already exfiltrated.
Variant 2: Form Overlay (Dynamic Fake Form Injection)
Where the DOM Monitor is passive (it only observes), the Form Overlay attack is active: it replaces the legitimate payment form with a visually identical fake.
The Injection Technique
The overlay is injected with a high z-index so it appears to be the real form:
| |
The real form is hidden (not removed) so the bank’s own JavaScript — form validation, session management, API calls — continues to work. When the victim submits the fake form, the overlay captures the data, removes itself, and programmatically triggers submission on the real hidden form. The legitimate transaction completes normally.
Why This Is Hard to Detect
From a victim’s perspective: the page looks identical, the transaction succeeds, no error occurs. From a DevTools perspective: the form the victim sees is a dynamically created DOM element with no source file. It will not appear in the Sources panel.
Variant 3: Shadow DOM Skimmer (Maximum Stealth)
The Shadow DOM Skimmer is the most advanced variant. It uses the browser’s Shadow DOM API to hide its attack infrastructure in a closed shadow tree — genuinely invisible to document.querySelector, browser extensions, and most security scanners.
Shadow DOM Basics
A shadow root attached with { mode: 'closed' } cannot be accessed from outside:
| |
What Lives in the Shadow
The attacker’s monitoring infrastructure — event listeners, data buffers, exfiltration functions — all live inside the shadow tree. From the main document, there is nothing to find:
| |
Cross-Boundary Event Monitoring
Events dispatched on elements inside a shadow tree do bubble out, but their target is retargeted to the shadow host. The attacker hooks into the host’s event listeners and uses composedPath() to find the real target:
| |
Why
composedPath()? Event retargeting hides the real origin of shadow DOM events from external observers.composedPath()is one of the few APIs that can pierce the shadow boundary — attackers use it deliberately.
Anti-Analysis: Hooking attachShadow
The Shadow DOM Skimmer overrides the native attachShadow to monitor every shadow root created on the page — including those from legitimate web components:
| |
Lab 2 Technical Walkthrough
File Structure
02-dom-skimming/
├── vulnerable-site/
│ ├── banking.html # SecureBank target (base href="/lab2/")
│ ├── js/banking.js # Legitimate banking application (26.8 KB)
│ ├── css/banking.css
│ └── malicious-code/
│ ├── dom-monitor.js # Variant 1: Real-time field monitoring (18.9 KB)
│ ├── form-overlay.js # Variant 2: Dynamic form injection (26.6 KB)
│ └── shadow-skimmer.js # Variant 3: Shadow DOM stealth (24.7 KB)
├── c2-server/
│ ├── server.js # Express.js C2 (port 3000/8080)
│ ├── dashboard.html # Real-time stolen data viewer
│ └── stolen-data/ # Captured payloads (timestamped JSON)
└── test/
└── tests/
├── dom-monitor.spec.js # 5/6 tests pass (83%)
├── form-overlay.spec.js # 7/7 tests pass (100%)
└── shadow-skimmer.spec.js # 2/3 tests pass (67%)
Running the Lab
| |
The demo command starts the SecureBank site (nginx, port 8080), the C2 server (Express.js, port 3000), and Playwright tests in headed mode so you can watch the attack run in a visible browser window.
Observing the Attack in DevTools
Network tab — filter by /collect. POST requests appear every 5 seconds while typing. Each payload contains the partial card number captured so far.
Sources tab — locate dom-monitor.js and search for exfilUrl. Search attachShadow to find the shadow-skimmer initialization.
Elements tab — for the Shadow DOM variant, look for zero-sized <div> elements appended to <body>. They show #shadow-root (closed) but cannot be expanded.
Console tab — [DOM-Monitor], [Shadow-Skimmer], and [FormOverlay] prefixed messages reveal attack state. Production attacks disable this logging.
Key Detection Signatures
1. MutationObserver Targeting Payment Fields
| |
Legitimate use of MutationObserver rarely needs subtree: true on document itself and almost never watches autocomplete attribute changes.
2. Aggregated Event Listeners on Input Fields
| |
Legitimate validation usually needs only input or change. keydown + keyup + input + paste together is a strong keystroke-logging signal.
3. Closed Shadow DOM with Zero-Sized Host
| |
4. Periodic POST Requests to Non-Payment Domains
Any setInterval that triggers a fetch POST to a non-payment-processor domain during a banking or checkout session is a critical indicator.
Detection Methods
Browser DevTools
- Network tab → XHR/Fetch filter → POST requests to unexpected domains
- Sources tab → search scripts for:
MutationObserver,attachShadow,composedPath,setInterval.*fetch - Memory tab → heap snapshot → look for detached DOM trees with attached event listeners
Static Analysis
| |
Semgrep Rules
| |
Prevention Strategies
Content Security Policy: connect-src Is the Critical Directive
For DOM-based attacks, script-src is necessary but not sufficient — the malicious code is already loaded. What blocks exfiltration is connect-src:
| |
Any fetch or beacon to a non-allowlisted domain will be blocked. This stops all three Lab 2 variants from successfully exfiltrating data — even if the skimmer code runs.
Payment Iframes
Isolating payment forms inside cross-origin iframes means injected JavaScript on the parent page cannot access the iframe’s DOM. The skimmer cannot attach event listeners to fields it cannot reach.
| |
Additional Defenses
- Trusted Types API — prevent DOM injection that could load skimmer scripts
- Runtime Application Self-Protection (RASP) — monitor
MutationObserverandattachShadowcalls at runtime - CSP violation reporting — deploy
report-onlymode first to understand baseline, then enforce - HTTP Observatory — regularly test your CSP headers at observatory.mozilla.org
Key Takeaways
- DOM-based skimming captures data character-by-character, before form submission
MutationObserverallows skimmers to persist across dynamic UI changes and SPA navigation- Closed Shadow DOM creates genuinely invisible attack infrastructure
connect-srcCSP is the most effective single control — it blocks exfiltration even if skimmer code runs- Cross-origin payment iframes eliminate the DOM attack surface entirely
Try It Yourself
Lab 2 lets you:
- Switch between all three attack variants using the
LAB2_VARIANTenvironment variable - Watch live keystroke capture in the C2 dashboard as you type into SecureBank forms
- Run Playwright tests in headed mode to see the attacks automated
- Practice detection using DevTools and the static analysis patterns above
Continue learning:
- Lab 1: Basic Magecart — classic form-submit skimming
- Lab 3: Browser Extension Hijacking — supply chain attacks via extension compromise
- MITRE ATT&CK Matrix — T1185 Browser Session Hijacking, T1056.004 Credential API Hooking
- Interactive Threat Model — visualize the full attack surface
We’re participating in Google Summer of Code. Help us build new detection tooling, lab scenarios, or ML-based skimmer classifiers.