To detect malicious JavaScript on an ecommerce site, start with what the code has to accomplish rather than what it looks like. A skimmer has three jobs. It must load on the page where card details are typed. It must read the values out of those fields. It must send them somewhere. Each of those three jobs leaves a trace, and the traces are visible from a browser, a code repository and your platform admin without any monitoring product.
What follows is an ordered pass of seven checks. Work through them in sequence. By the end you will have three artefacts: an inventory of every script on your payment page, a saved network trace of a test transaction, and a baseline you can compare against next month. Those artefacts also happen to be most of what PCI DSS 4.0 asks of a payment page.
Before you look: a single desktop page view proves nothing
Skimmers stopped running unconditionally years ago. The code that researchers pull apart now tends to check its surroundings first. Common gates include a URL test for strings such as checkout, onepage, payment or basket, a cookie that lets the payload fire once per session and stay silent afterwards, a user agent test that only targets mobile browsers, and a developer tools check that pauses execution when a window is open.
Read plainly, that is: the moment you open DevTools on your desktop and load the checkout, you may be the one visitor the skimmer has been written to ignore.
So set up properly before you start.
- A fresh browser profile or incognito window, with ad blockers and privacy extensions disabled, so you see every request the real customer sees.
- A second pass with the device toolbar set to a mobile profile, and ideally a real phone on mobile data.
- A genuine route to the payment step. Add to basket, proceed, reach the card form. Deep-linking to the checkout URL sometimes skips the scripts that load in sequence.
- A test card from your payment provider, or a sandbox order flow, so you are not pushing a live authorisation through a page you suspect.
- Preserve log enabled in the Network panel, so nothing is wiped by a redirect.
Check 1: Inventory every script the checkout actually loads
Open the Network panel, filter to JS, and walk the basket, checkout and order confirmation pages. Then open the Sources panel and read the tree. The Network list shows what was fetched. The Sources tree shows the structure, including scripts appended to the DOM at runtime by another script.
Record four things per script: the domain it loads from, what it does, who inside the business owns it, and whether it is your code or a supplier’s. A spreadsheet is fine. That list is the artefact PCI DSS 4.0 requirement 6.4.3 asks for anyway: every script on the payment page, authorised, with a written reason for being there.
Two patterns deserve a flag immediately. Scripts that appear only after you interact with the card fields, and chained loads where one approved tag fetches a second file from a domain nobody has heard of. A tag manager container is the usual culprit for the second pattern, and it is doing exactly what it was built to do.
Anything on the list that no one in the business can name or justify goes in a separate column. That column is where you spend the rest of the afternoon.
Check 2: Read your own first-party files
Most merchants hunt for an unfamiliar filename. British Airways did not have one. Between late August and early September 2018, the code that stole card details from ba.com sat inside a modified copy of Modernizr, a legitimate script that was already on the page, with the stolen data posted to a look-alike domain. The ICO fined the airline £20m in October 2020 over the breach. The file name looked right. The contents did not.
So check timestamps first. Sort your theme and JS directories by modification date and look for anything changed outside a deployment window. Then grep for the vocabulary of exfiltration:
- atob, unescape, String.fromCharCode, eval, long hex or base64 blobs
- addEventListener bound to keydown, blur, change, submit or beforeunload
- WebSocket, navigator.sendBeacon, new Image(), fetch to a hard-coded host
Grep the compiled bundles too, not only the source. And look where files are not files: inline script blocks rendered by the CMS, content blocks and CMS pages stored in the database, layout XML, and the header and footer script fields in the platform admin. On Magento and Adobe Commerce, Content > Design > Configuration holds HTML that never appears in a repository diff.
If you find an encoded blob, decode it away from the browser. Copy the string into a file and run it through a command line base64 decoder or a local copy of CyberChef. Do not paste it into the console of the page you are investigating.
Check 3: Submit a test transaction and follow the data out
This is the check that produces proof. Enter a test card and a distinctive cardholder name, something like ZZTESTNAME, then search the traffic for what you typed.
Exfiltration rarely looks like a POST to a sinister endpoint. Crews favour a field blur handler that fires the moment focus leaves the card number input, packs the value into base64 and requests an image with the data in the query string. Others use sendBeacon during page unload, which survives navigation. Some open a WebSocket and push a single frame. If you only watch the form submit, you will see nothing.
Use the search function in the Network panel, not your eyes. It searches headers and bodies across every recorded request. Search the card digits. Search ZZTESTNAME. Search for the base64 encoding of both, since the data may already be wrapped by the time it leaves. Then check the WS filter for frames, and the Img filter for GETs with improbably long query strings.
Look-alike domains are the giveaway when you do find something: a cdn- or stats- subdomain that resembles your own brand or your payment provider’s, registered weeks ago. Repeat the whole pass on mobile emulation and from a different IP address, because geo-fencing and device gating are standard.
Check 4: Compare the page against a known-good baseline
Detection without a baseline is guesswork. Build one while the page is believed clean, and store it off the web server: SHA-256 hashes of every theme and bundle JS file, a saved HAR of a full checkout, and the approved inventory from Check 1.
Subresource Integrity turns a silent supplier change into a visible load failure, which is precisely what you want and also an operational headache. When the supplier updates the file legitimately, the hash stops matching and the script simply does not run. Pin only the scripts whose absence the payment page can survive, and agree a change-notice process with the vendor. Otherwise the first broken checkout ends with someone stripping the integrity attribute out permanently at 11pm.
The cheapest sensor on this list is Content Security Policy in report-only mode. It blocks nothing, breaks nothing, and reports every script source and connect destination that was not in your policy. Point report-uri or report-to at a collector you control, deploy it on the payment page, and you have a working change-detection setup for the checkout. The honest catch: reports accumulate, mostly from browser extensions, and somebody has to read them weekly. An unread report queue is not a control.
File hashes plus a report-only CSP plus a dated script inventory covers requirements 6.4.3 and 11.6.1, both mandatory since 31 March 2025, without a licence fee. The full standard is published by the PCI Security Standards Council, and the wording of 11.6.1 matters: the mechanism must detect unauthorised change to the payment page as received by the consumer browser. Server-side file integrity monitoring alone does not meet that, because supplier code never touches your server.
Check 5: Separate a nosy marketing tag from a real skimmer
The most common wasted afternoon in this process is a false positive. Session replay, heatmaps, A/B testing tools and chat widgets genuinely read form inputs and genuinely send data to third-party domains. On the Network panel that is the same shape as a skimmer.
Four questions settle it quickly. Is the destination a documented endpoint for a vendor you have a contract with? Is the field masked or excluded at capture, verified by searching the payload for your test digits rather than trusting the vendor’s dashboard setting? Was the tag approved, by whom, and on what date? And does the contract permit that vendor’s code on a payment page at all?
That last question is where Ticketmaster UK came unstuck in June 2018. The malicious code arrived through a compromised third-party chatbot script that was loading on payment pages it was never intended to reach. The supplier was legitimate. The placement was not.
Which brings the worst finding in this whole process, and it is not malware. A tag manager with open editing access, no approval workflow and no audit trail means anyone with the login can publish JavaScript to your checkout in seconds. Nothing malicious present today proves nothing about tomorrow. Fix that before you fix anything else.
If a payload search returns your test card digits going to a domain nobody can account for, stop investigating. You are now running an incident, and the confirmation steps and sequencing for a suspected skimmed checkout matter more than further analysis.
Check 6: Find the entry route, not just the code
Finding the file is half the job. There are three routes in: a compromised supplier script, injected first-party code via stolen admin or server access, and an unpatched platform or extension. Sansec reported roughly 1,900 Magento 1 stores skimmed over a single weekend in September 2020, which remains the clearest example of the platform route at scale.
On the platform side, check for admin users and API keys nobody recognises, recent extension installs, modified core files against a clean checkout of the same version, scheduled tasks and cron entries, and database-level persistence such as unexpected triggers or stored procedures that rewrite content after a cleanup.
On the supplier side, establish who can publish code that reaches your checkout, what their change process is, and whether each script still needs to be there at all. The quickest win in this entire article is deleting tags that were added for a campaign that ended in 2023.
When cleaned code reappears within days, the entry route is still open, and the persistence is almost always in the database or a scheduled task rather than in the theme files you fixed.
Check 7: Treat outside signals as detection too
The first warning usually comes from outside. Acquirer and issuer fraud reports, common point of purchase alerts naming your store, and clusters of small card-testing authorisations tend to arrive before any internal alert fires. Take a customer report of a payment form that looked slightly different, double-submitted, or asked for the CVV twice as a technical signal, not a usability complaint.
Add a scan from a clean machine on a different network, and check your domains against public blocklists and skimmer domain intelligence. Then set a rhythm: re-run the script inventory and the baseline comparison after every deployment and every supplier update. Monthly for a stable store. Weekly if marketing has tag manager access.
When a check comes back dirty
Preserve evidence before you clean. Copy the modified files intact, export the HAR, pull server access logs, admin login records and the platform audit trail, and write down the timestamps. Cleaning first destroys the only record of what happened and when, which is the record your acquirer will ask about.
Then reduce exposure. Moving card inputs into hosted payment fields served by your provider takes the card number out of your DOM entirely, so injected JavaScript on your page cannot read it. It does not fix the compromise, does not stop attackers overlaying a fake form on top of the iframe, and does not remove your inventory and change-detection duties. Tell your acquirer and payment provider early, and assess UK GDPR notification duties to the ICO on the 72-hour clock.
The short version
A skimmer must load, read and send. Inventory the scripts, read your own files for injected code, submit a test card and search the outbound traffic for the digits you typed, then hold the page against a baseline of hashes, a HAR and a report-only CSP. Run the pass on mobile as well as desktop, resolve the marketing tags before you panic, and close the entry route or the code comes back. Done properly, that is how you detect malicious JavaScript on an ecommerce site and keep the evidence that proves you looked.
Frequently Asked Questions
How do I detect malicious JavaScript on an ecommerce site without buying a security tool?
Browser DevTools, a grep across your own JavaScript, file hashes and a report-only Content Security Policy cover the ground at no licence cost. The DevTools pass finds exfiltration in a test transaction, the grep finds injected code in first-party files, and the hashes plus CSP reports show when the page changes. The cost is time and the discipline to read the reports.
What does a skimmer look like in the browser DevTools Network panel?
Usually unremarkable. Look for requests fired on field blur rather than on submit, image GETs carrying long base64 query strings, sendBeacon calls during unload, and WebSocket frames. The reliable method is not visual: search every request body and header for the card digits and cardholder name you typed, and for their base64 encodings.
Can a skimmer hide inside Google Tag Manager or an analytics script?
Yes, and it is a favoured route because the container is trusted and often loosely governed. A custom HTML tag can inject any script, and one approved tag chain-loading a second domain is hard to spot without a script inventory. Restrict publish rights, require approval, and keep a dated record of every tag that reaches the payment page.
Do hosted payment fields mean I no longer need to check my checkout scripts?
No. Hosted fields keep the card number out of your DOM, which removes most of the skimming risk, but attackers can still overlay a convincing fake form, alter the page around the iframe, or harvest name, address and email. PCI DSS still expects an authorised script inventory and change detection for the page that delivers the payment form.
How often should I re-check my payment page for injected code?
Re-run the inventory and baseline comparison after every deployment and every supplier script update, and at least monthly otherwise. If a report-only CSP is in place, read the violation reports weekly, since that is the control doing the continuous watching between manual passes.
