A customer emails to say their card was used fraudulently soon after they bought from you. Then another. Or your payment processor or bank tells you that your store looks like the common point of purchase for a group of stolen cards. Maybe you noticed it yourself: the checkout suddenly asks for card details twice, a card form appears that does not look like your gateway’s, or a security scanner flags JavaScript on your checkout page.
These are the signs of a card skimmer (sometimes called a Magecart-style or formjacking attack): malicious code planted on your store that copies what shoppers type into the checkout and sends it to the attacker. Unlike card testing, which only abuses your public checkout, a skimmer means someone has write access to your site, its files or its database. It is a security incident and, very likely, a data breach with legal and payment industry duties attached.
This guide is for store owners and the developers who support them. It walks through containing the damage, finding the skimmer in the places researchers keep finding them, removing it, closing the way in and telling the right people. It is technical guidance, not legal advice. Your duties depend on where you and your customers are and on your contracts, so check them with a qualified adviser.
Do these three things first
- Stop taking card payments on the site. Put the store into maintenance mode, or disable the card payment methods in WooCommerce > Settings > Payments, until you know the checkout is clean. Every order taken while the skimmer runs is another stolen card.
- Keep evidence before you clean anything. Take a full copy of the files and a database export as they are now, label them infected, and store them away from the server. Download the access and error logs before they rotate, and ask your host for theirs. The hack recovery checklist covers this.
- Start a timeline. Note when you first heard of a problem, what you saw and every change you make, with times. Processors, investigators and regulators will ask for it.
What is happening
A skimmer needs to see card data before your gateway does. Public research from security firms shows three broad kinds on WordPress and WooCommerce stores.
| Kind |
How it works |
Documented examples |
| Browser-side JavaScript |
Script loads on the checkout page, reads the form fields as the shopper types or submits, and sends the data to a server the attacker controls. Often encoded or encrypted so it does not look like card data in transit. |
Sucuri (January 2025) described a skimmer stored in the WordPress database in the widget_block option of the wp_options table, inserted as an HTML block widget, which only ran on URLs containing “checkout” and either hijacked the real fields or drew a fake payment form. |
| Fake payment form |
The script hides or covers the real payment section and shows a convincing copy, sometimes styled like Stripe. The shopper types the card into the fake form; some variants then show an error so the shopper enters it again in the real one, and the order goes through normally. |
The same Sucuri case; several skimmers imitate hosted card fields. |
| Server-side PHP |
Code on the server reads the checkout POST data or adds extra card fields to the billing form, then sends the data out from the server itself. Nothing suspicious appears in the shopper’s browser. |
Sucuri (2024) found a skimmer installed through a little-known snippets plugin, with code saved in the dnsp_settings option. Wordfence (2025) analyzed fake plugins, one calling itself “WordPress Core”, that carried skimmers and backdoors, and another campaign that hid payloads in files with image extensions. |
Two patterns matter for your search. First, skimmers increasingly live in the database (options, widgets, snippet plugins) rather than in files, so a file scan alone can miss them. Second, many hide from administrators: they only run for logged-out visitors, only on the checkout, or only once per visitor. Wordfence also reported a fake plugin that marked fraudulent orders as completed to delay detection.
Hosted card fields help but do not make you immune. If you use Stripe, WooPayments or PayPal advanced card fields, the real card number is typed into a frame served by the processor, which page scripts cannot read. A skimmer gets round that by drawing its own fake fields on your page, or by adding fields to your billing form. That is why the checks below look for fields that should not be there.
Step 1: Look at the checkout as a shopper
- Log out of WordPress, or use a private window in a browser where you have never logged in. Ideally use a different network too, such as a phone on mobile data shared to your laptop.
- Add a product to the cart and go to the checkout.
- Open DevTools (F12, or Cmd+Option+I on a Mac), choose the Network tab and check Preserve log.
- Fill in the billing form with fake details and type a test number such as
4242 4242 4242 4242 into the card field. Do not place the order.
- Watch the Network list as you type and as you move between fields. Requests to domains you cannot account for (not your own domain, not your gateway, not services you know you use) are the red flag. Click one and read its Headers and Payload tabs.
Then check where the card field really lives. Right-click inside the card number box and choose Inspect. On a Stripe or WooPayments store, the box should sit inside an <iframe> whose address starts with https://js.stripe.com/. PayPal’s card fields sit inside a frame from a PayPal domain. If the card number box is an ordinary <input> in your page while your gateway uses hosted fields, or you see two card forms, treat that as a skimmer until proven otherwise. Some older gateways legitimately use direct fields, so compare with your gateway’s documentation.
Finally, view the page source (Ctrl+U, or Cmd+Option+U on a Mac) and search for <script. List every external script domain. The DevTools cheat sheet shows how to filter the Network tab by domain.
A clean result here does not clear the site. Server-side skimmers show nothing in the browser, and some only run for certain visitors. Carry on with the server checks.
Step 2: Search the database
Connect over SSH and cd into the site folder (the one with wp-config.php). Use wp, or wp-cli if that is what your host provides. These commands only read. First find your table prefix, because not every site uses wp_:
wp db prefix
Then search the tables where skimmers have been found. Replace wp_ below with your prefix.
wp db search '<script' wp_options --stats
wp db search 'eval\(|atob\(|fromCharCode|unescape\(' --all-tables-with-prefix --regex --stats
wp option get widget_block --format=json
What each does: the first line looks for script tags stored in the options table and --stats prints how many matches it found and where. The second searches every table with your prefix (--all-tables-with-prefix) for functions that obfuscated JavaScript commonly uses; --regex treats the pattern as a regular expression where | means “or”. The third prints the content of every block widget, where the 2025 Sucuri skimmer lived. Good output: few or no matches, each one something you recognize, such as a tracking snippet you added. Bad output: long encoded strings, scripts loading from domains you do not know, or anything that mentions checkout, card or payment fields. Legitimate page builders and minified libraries can also match these patterns, so read each hit rather than deleting on sight.
Check code snippet and header script plugins
Snippet plugins run whatever code is saved in them, which makes them a favorite hiding place. Look at every snippet, active or not:
wp plugin list --fields=name,title,status,version
wp db query "SELECT id, name, active, modified FROM wp_snippets ORDER BY modified DESC LIMIT 20;"
wp post list --post_type=wpcode --post_status=any --fields=ID,post_title,post_status,post_modified
What each does: the first lists all plugins with their display names, so you can see which snippet or header script plugins exist. The second applies to the Code Snippets plugin, which keeps snippets in its own table; it lists the 20 most recently changed. The third applies to WPCode, which stores snippets as a hidden post type. If a command reports that the table or post type does not exist, that plugin is not installed and you can skip it. In the admin, also open any “header and footer scripts” settings, your theme’s custom code boxes and your page builder’s custom code area. Any snippet you did not write, or one changed around the time trouble began, needs a close look.
Check recently changed content and admin users
wp post list --post_type=any --post_status=any --orderby=modified --order=DESC --posts_per_page=20 --fields=ID,post_type,post_title,post_modified
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
What each does: the first lists the 20 most recently modified items of any type, including hidden ones. The second lists every administrator with the date they were created. An administrator you do not recognize, created recently, is how many attackers get back in. FG-84 on compromised admin accounts explains what to do with one.
Step 3: Check the files
wp core verify-checksums
wp plugin verify-checksums --all
wp plugin list --status=must-use
wp plugin list --status=dropin
What each does: wp core verify-checksums compares every WordPress core file with the official copy and lists any that differ. wp plugin verify-checksums --all does the same for plugins from the WordPress.org directory; premium plugins will report that they cannot be verified, which is expected and not a problem in itself. The last two list must-use plugins and drop-ins, which load automatically and do not show on the normal Plugins screen. Good output: Success: WordPress installation verifies against checksums. and no unexpected must-use plugins. Bad output: changed core files, plugins that fail verification, or must-use plugins nobody installed.
Look for plugins you did not install, especially ones with names that imitate WordPress itself or another plugin. Then look for recently changed code and for code where it should not be:
find . -type f \( -name '*.php' -o -name '*.js' \) -mtime -30 -not -path './wp-content/cache/*' | head -n 100
find wp-content/uploads -type f -name '*.php'
find wp-content -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.gif' -o -name '*.ico' \) -exec grep -l '<?php' {} +
find wp-content/themes -path '*woocommerce/checkout/*'
What each does: the first lists PHP and JavaScript files changed in the last 30 days (-mtime -30), skipping the cache folder; widen the number if the problem is older. Attackers can fake file dates, so a quiet result is not proof. The second finds PHP files in uploads, where none should exist. The third finds image files that contain PHP code, a trick documented in a 2025 Wordfence analysis. The fourth lists checkout templates your theme overrides; compare form-checkout.php and payment.php with the copies in the WooCommerce plugin, and check WooCommerce > Status, which lists template overrides. Run these from the site folder over SSH. The scanner false positive guide helps you judge a suspicious file.
A reputable malware scanner, run from the server or by your host, is a useful second opinion, particularly for known skimmer families. Do not rely on it alone; the database checks above find things file scanners miss.
Step 4: Remove it and close the way in
- Restore or rebuild. If you know roughly when the skimmer arrived and you have a clean backup from before that, restoring is the fastest route. You will lose orders placed since, so follow the restore guide to keep them. Otherwise, replace WordPress core, every plugin and every theme with fresh copies from their official sources, and remove anything you cannot trace to a source.
- Clean the database entries you found. Take a fresh backup first, then remove the malicious widget, option content, snippet or post through the admin where you can. Note exactly what you removed and where.
- Remove rogue users and access. Delete unknown administrators, revoke application passwords you did not create (Users > Profile > Application Passwords), and remove unknown FTP, SFTP and SSH accounts at the host.
- Change every secret. WordPress passwords for every administrator and shop manager, hosting, SFTP, database and email passwords, the security salts in
wp-config.php, and your gateway API keys and webhook secrets. In Stripe you can roll the secret key from the Developers area of the Dashboard; update the plugin straight after.
- Find and fix the entry point. An outdated plugin, a stolen admin password or a nulled theme are common causes. If you skip this, the skimmer comes back. Follow the reinfection guide and the plugin vulnerability guide.
- Purge every cache. Page caches and CDNs can keep serving the infected HTML after the site is clean. Purge the cache plugin, the server cache and the CDN.
- Retest Step 1 and Step 2 before turning card payments back on.
Step 5: Make the next skimmer harder to plant and easier to spot
A Content Security Policy for the checkout
A Content Security Policy (CSP) is a response header that tells browsers which domains a page may load scripts from and send data to. A strict policy stops a browser-side skimmer sending data to an unknown domain, and its reports tell you when something tries. Start in report-only mode so you can see what would break:
Content-Security-Policy-Report-Only: script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com https://hooks.stripe.com; connect-src 'self' https://api.stripe.com; report-uri https://example.com/csp-report
Placeholders: this is a starting shape for a store using only Stripe, not a finished policy. Add every domain your checkout legitimately uses (analytics, PayPal, address lookup and so on) from each vendor’s current documentation, and replace the report-uri address with an endpoint or reporting service that collects violation reports. Once the reports show only expected domains, switch the header name to Content-Security-Policy to enforce it. A CSP does not stop server-side skimmers or scripts that send data back to your own domain, so it is one layer, not the cure. The security headers cheat sheet explains where to set headers, and the headers tool confirms what your checkout sends.
Watch for change
- File integrity monitoring from a security plugin or your host, alerting you when core, plugin or theme files change outside an update.
- Alerts when a new administrator is created or a plugin is installed.
- A monthly look at snippet plugins, header script boxes and widgets, using the commands above.
DISALLOW_FILE_EDIT set to true in wp-config.php, so a stolen admin login cannot edit theme and plugin files from the dashboard.
- Two-factor authentication for every administrator and shop manager.
Step 6: Who to tell
This section is general information, not legal advice. Notification duties vary by country, state, contract and the data involved. Take advice from a lawyer or data protection specialist as early as possible; deadlines can be short.
Your payment processor
Tell Stripe, WooPayments (through WooCommerce support), PayPal or your merchant bank as soon as you have reason to believe card data was exposed. Card network rules, passed on through your merchant agreement, generally require prompt reporting of a suspected compromise, and the processor may ask for a forensic investigation by a PCI Forensic Investigator depending on the size of the incident. The exact duty and timeline come from your merchant agreement and vary by provider, so read it and ask your processor. Share your timeline, the date range, and what you found and removed.
PCI DSS
Every merchant that accepts cards has PCI DSS obligations, even when the processor’s hosted fields keep card numbers off your server. PCI DSS version 4 added requirements aimed at scripts on payment pages, and the PCI Security Standards Council has since adjusted how they apply to merchants who use hosted payment fields. Ask your processor which Self-Assessment Questionnaire applies to you and what it requires for your checkout page. After an incident, expect to be asked to re-validate.
Data protection regulators and customers
- UK and EU (GDPR and UK GDPR). A personal data breach must be reported to the supervisory authority within 72 hours of becoming aware of it, unless it is unlikely to result in a risk to people. Where the risk to people is high, they must also be told without undue delay (Articles 33 and 34). Whether stolen card details count as high risk in your case is a judgment to make with a lawyer or data protection specialist.
- United States. Every state has its own breach notification law, with different definitions of personal information, deadlines and requirements to notify state attorneys general. Check the rules for each state where affected customers live with a lawyer licensed in the US.
- Elsewhere. Canada, Australia and many other countries have mandatory breach notification schemes. The rules and deadlines vary by country, so check with a lawyer licensed where you operate.
- Customers. Work out which orders fall inside the window when the skimmer was live, using your evidence: the earliest backup that contains it, file or database change dates, and the first customer complaints. A clear notice tells them what happened, which dates are affected, what data was involved, that they should contact their card issuer, and how to reach you. Keep the wording factual; your adviser should review it.
Others
- Your cyber insurer, if you have one. Many policies require prompt notice and some require approved responders, so check the notice terms in your policy.
- Your host, who may find other affected accounts on the server and can supply logs.
- Law enforcement, through your national cybercrime or fraud reporting service, if your adviser recommends it.
If you cannot find it
If customers keep reporting fraud and nothing turns up, bring in a professional incident responder; a skimmer that survives your search may be server-side or loaded from outside WordPress. Send your host a ticket with:
- Your timeline and the date range of reported fraud.
- The results of the checksum, database and file checks above.
- Any unknown domains seen in the Network tab or the page source.
- A request for their malware scan, access logs for the checkout URL and a list of files changed in the window.
Also ask your gateway whether the reports could have another source. If fraud reports come only from orders that also show failed attempts and unknown origins, you may be looking at card testing rather than skimming.
See also