You log into your Stripe dashboard one morning and see hundreds (sometimes thousands) of $1 charge attempts, almost all declined. Stripe Radar is catching most of them, which is the good news. The bad news is that Stripe charges roughly $0.02 per Radar review for blocked card-testing attempts, and that quietly adds up. A 10,000-attempt overnight run is $200 nobody planned to spend. Your members are not seeing checkout problems and your bank is happy, but your Stripe bill is not.

This is a card testing attack. In this post, we will cover what is actually happening, how to figure out where it is coming from on your site, how to shut it down, and how to harden the site so it does not happen again.

Banner image for the Stripe Card Testing Attacks blog post — dark metallic abstract background with title 'Stripe Card Testing Attacks' and subheadline 'Diagnose, Prevent, Recover'

What is a Stripe Card Testing Attack?

Carders are people who buy or steal lists of credit card numbers. Before they can sell or use those numbers, they need to know which ones still work. Hitting a real merchant’s payment endpoint with each card and watching whether the charge goes through is the cheapest way to validate them at scale. They are not trying to actually buy anything from you. The card testing process looks like this:

  1. Pick a target site that accepts Stripe.
  2. Send a small payment attempt (commonly $1, sometimes $0.50 or whatever your lowest level costs).
  3. If it goes through, the card works. Add it to the “live” pile.
  4. If it declines, throw the card out.
  5. Loop, with a different IP, browser fingerprint, or rotated card every few attempts.
Anatomy of a card testing attack: pick a target site, send a $1 charge, save approved cards, discard declined cards, loop with a new IP
The five-step loop every card testing attack follows.

Stripe’s own data shows the trend has shifted from random enumeration (guessing card numbers) toward verification attacks against dumps of already-stolen card numbers, which is why authorization rates on these attempts have actually gone up over the past couple of years. (Source: Stripe Radar response to card testing.)

Where the Attack is Coming From

There are three plausible entry points on a WordPress site. You can diagnose by elimination.

Three entry points for a card testing attack: your WordPress checkout, direct to Stripe via your publishable key, compromised plugin or theme or admin
The three plausible entry points to diagnose by elimination.

Your WordPress checkout

If the attempts are going through your normal checkout flow, you will see them in your plugin’s own order or entry log:

  • Paid Memberships Pro: Go to Memberships > Orders and filter by status error, review, or token. Look for a sudden spike of $1 attempts with random first and last names and rotating email addresses.
  • WooCommerce: Go to Orders > All and filter by “Failed” and a date range.
  • Gravity Forms + Stripe Add-On: Go to Forms > Entries, filter by form, and sort by Stripe transaction status.
  • Easy Digital Downloads: Go to Downloads > Payments > Failed.
Paid Memberships Pro Orders admin filtered to status 'token' showing a spike of low-dollar test charges with random emails like test123@strangerstudios.com and dev-email+Caroline.Ortiz@flywheel.local
A spike of “token” status orders with random first/last names and rotating emails is the most common signal in Paid Memberships Pro.

If you see the spike in your plugin’s records, the attacker is going through your form. The fix is on your site.

Direct to Stripe via your publishable key

If the attempts appear in Stripe but not in any plugin’s order log, the attacker is calling Stripe’s API directly using your publishable key (pk_live_...). They never touched your site to make the charge. They only had to visit it once to scrape the key from your page HTML.

Stripe says publishable keys are “safe to expose outside your backend.” That is true in the strict sense. A pk_live_ key cannot fetch customer data or refund anything. But Stripe also acknowledges that card testers can use a publishable key to retry a large number of payments. (Source: Stripe docs on preventing card testing.) This is the most common quiet-but-expensive attack on small WordPress merchants.

Compromised plugin, theme, or admin

If you find unauthorized requests in your Stripe API logs from a server IP you do not recognize, or charges with unusual metadata, the attacker may have your secret key (sk_live_...). That is a much bigger problem. You will see it in Stripe’s API request logs. Search for requests not coming from your own server.

Find Everything That Connects to Stripe

Before you can fix anything, list every connection. A typical WordPress site has more than people think.

Where to lookWhat to check
WP-Admin plugin listAnything with “stripe” in the name. Common: WooCommerce Stripe, Gravity Forms Stripe Add-On, WP Simple Pay, Stripe Payments, GiveWP Stripe, Easy Digital Downloads Stripe Pro, Forminator Stripe, MemberPress Stripe, LearnDash Stripe
Memberships > Settings > PaymentsSelected gateway, and whether it is connected via Stripe Connect (recommended) or via manual API keys
WordPress options table (wp_options)Search keys containing stripe. Use wp option list --search='*stripe*'
wp-config.phpAny define('STRIPE_…') constants that hardcode keys
Custom theme functions.phpDirect API calls. Grep for pk_live, sk_live, rk_live
Code Snippets plugin (wp_snippets)A custom snippet doing direct Stripe calls
External integrationsZapier, Make, n8n, MailerLite, Kit triggers, Slack notifications. Anything that has a Stripe restricted key or webhook secret
Stripe Dashboard > Developers > API keysThe full list of every key issued, with last-used timestamps
Stripe Dashboard > Developers > WebhooksEvery URL Stripe sends events to. This should be your site plus your integrations, and nothing else

Anything in that list with a stored secret or restricted key is an attack surface to harden or remove.

Diagnose: Site vs. Direct-to-Stripe

Here is a simple test you can run during or right after an attack window:

  1. Count attempted payments in your plugin’s order log during the attack window.
  2. Count attempted payments in your Stripe Dashboard under Payments, with the “Incomplete” and declined filters on, during the same window.

If the Stripe number is much higher than the plugin number, your publishable key is being used directly. If the two numbers match, the attacker is going through your site.

You can also tail the Apache or nginx access log on the site for POST requests to your checkout endpoints during the attack window. If there is no traffic spike there but Stripe shows attempts, the attacker is hitting Stripe’s API directly, not your form.

How Stripe Connect Helps

Stripe Connect is the OAuth-based way to link a Stripe account to a third-party platform. Paid Memberships Pro, GiveWP, and many other WordPress plugins use Connect by default. Here is how Connect changes the attack surface compared to manual API keys:

  • Your secret key never lives in WordPress. With Connect, the plugin does not ask for sk_live_…. The platform (in our case, PMPro) is authorized via OAuth to act on your account, and the platform’s secret key, held by the plugin author and not by you, is what actually makes API calls on your behalf. If your WordPress database is dumped, the attacker does not get sk_live_… because it was never there.
  • The plugin can use Stripe-hosted Checkout. With Connect, the plugin can create a “Checkout Session” server-side and redirect the buyer to checkout.stripe.com. The publishable key never has to appear in your page HTML at all. Stripe’s hosted page handles it. If the customer never sees a publishable key on your site, no carder can scrape it.
  • You can disconnect and reconnect. If something gets weird, you can revoke Connect in your Stripe dashboard under Settings > Connected Accounts, then reconnect from your plugin. New scoped tokens, fresh OAuth flow, and the attacker’s stale keys are dead.

Connect does not make your publishable key “secret.” It is still public by design. But it lets the plugin use Checkout-flow integrations that do not expose the key in your page HTML in the first place.

How Restricted Keys Help

If you are not using Connect, for example if you are integrating Stripe from custom code, a Zapier workflow, or a third-party service that asks for a key, use a restricted key (rk_live_…) instead of your secret key.

  • A restricted key has a permissions matrix you set at creation time. You can grant only read on Payments and nothing else.
  • If the restricted key leaks, the attacker gets exactly the access you granted, and nothing more.
  • Restricted keys can be rotated individually without touching anything else.
  • Stripe’s current recommendation is to use restricted keys wherever possible and save secret keys for the few server-side operations that truly need full account access. (Source: Stripe documentation on restricted API keys.)

If you have an old Zapier connection that is still using a full secret key, replace it with a restricted key scoped to just the resources Zapier needs. Same with Make, n8n, and any other automation.

Find and Inspect Your Keys in Stripe

Inside your Stripe dashboard:

  1. Developers > API keys. You will see every standard key (publishable, secret) and every restricted key. Each row shows the last-used time and last-used IP. If a key shows recent use from an IP you do not recognize, treat it as compromised.
  2. Developers > Logs. Filter by API method, key used, IP, and date range. A sudden burst of POST /v1/payment_intents calls from an IP outside your server is the smoking gun for a leaked publishable key.
  3. Payments > All payments, with the Incomplete and Failed filters on. Group by card brand and country. Card-testing runs usually have a very different country distribution from your normal traffic.
  4. Radar > Reviews and Radar > Early Fraud Warnings. Stripe is doing the work. The question is whether you are paying for it because the attacker is volume-attacking your endpoint, not because your business is growing.
Stripe Dashboard view of a single Block rule showing a $100 blocked payment with the rule condition 'If risk_level = highest' and a bar chart of matched payments
A single Block rule view showing the matched payment it caught. Multiply this by thousands during an active attack.
Stripe Dashboard transaction view of a $100 USD succeeded payment with Risk level tagged Elevated in the Details panel
An individual transaction Radar flagged as elevated risk. Useful for spot-checking what is making it through.

Expire or Rotate a Key

Stripe does not have a “delete” button for standard keys. The flow is:

  1. Go to Developers > API keys, open the overflow menu on the key you want to retire, and click Rotate key.
  2. Stripe creates a new key and asks how long the old key should remain valid: Immediately, in 1 hour, 24 hours, etc.
  3. Choose Expire now if you have already updated every integration using that key. Otherwise, choose a grace window (24 hours is common), update all integrations during that window, then rotate again with Expire now to kill the old key for sure.
  4. Save the new key value somewhere safe before closing the dialog. Stripe only shows it once.
  5. Update every place the old key was used. This is where the inventory you made earlier earns its keep.

For restricted keys, the same flow applies, but Stripe will also let you straight-up delete them. If a restricted key is not used anywhere anymore, delete it.

Stripe Dashboard Developers > API keys with the overflow menu open on a restricted key showing options Copy API key ID, Rotate key, Edit key, Duplicate key, Manage IP restrictions, View request logs, and Expire key
Open the overflow menu on the key you want to retire to find Rotate key.

Disconnect and Reconnect Stripe Connect

If you are using Connect (PMPro, GiveWP, etc.) and you suspect the connection is compromised, you can disconnect:

  1. Go to your Stripe Dashboard > Settings > Connected accounts (or Authorized applications).
  2. Find the platform (for example, “Paid Memberships Pro”).
  3. Revoke access.
  4. In WordPress, go back to the plugin’s settings page and click Connect Stripe again. You will be sent through Stripe OAuth and a fresh set of credentials will be issued.

Important gotcha: disconnecting can break webhook routing for other plugins that share the same Stripe account. The most common case is the Gravity Forms Stripe Add-On. Its webhook endpoint is registered separately from PMPro’s, but both depend on your Stripe account’s webhook configuration. After reconnecting:

  • Check Stripe Dashboard > Developers > Webhooks. Confirm every webhook endpoint you expect is still there and pointing at your site.
  • In each plugin that uses Stripe webhooks, re-verify the signing secret. Gravity Forms Stripe stores it per-feed. PMPro stores it under the gateway settings. WooCommerce Stripe stores it under the connection settings.
  • Make a test transaction (Stripe’s test mode is fine) and confirm the webhook fired and the plugin processed it.

If you skip this step, a real customer’s successful payment might not register in WordPress until you fix the webhook, even though Stripe charged the card. That is the same failure mode as duplicate-checkout bugs. Easy to miss, expensive to ignore.

Scan for Malware

If the attacker had your secret key, the question becomes how they got it. Common answers:

  • A theme or plugin file was modified to exfiltrate options to a remote URL.
  • An admin account was compromised, and someone copied the key from the Stripe gateway settings page.
  • An external service you gave the key to (a backup tool, a Zapier connection) was breached.
  • Your wp-config.php or .env was readable by an attacker who got code execution on the box.

Run a full malware scan on the WordPress install. Tools that work:

  • Wordfence (free or paid): file scan and real-time signature matching.
  • Sucuri SiteCheck (external scan), plus the Sucuri scanner plugin for an internal scan.
  • For Paid Memberships Pro Hosting customers, ask the team to run our internal scan against the droplet. There is an SOP for it.
  • On the server: find /var/www -name "*.php" -newer /etc/passwd to spot recently-modified PHP files outside your normal release flow. Look at the admin user list for any account you do not recognize.

If the scan finds anything, treat the entire Stripe key surface as compromised, and roll every key, not just the one you noticed in the logs.

Layered Defenses Recap

In rough order of bang-per-buck:

7 layered defenses against card testing: Stripe Connect Checkout, enable Stripe Radar rules, bot challenge on forms, rate-limit payment endpoints, use restricted keys, inventory every key, monitor Radar costs
Seven defenses in rough order of bang-per-buck.
  1. Use a Connect-based plugin with Stripe Checkout. An off-site flow means the publishable key is not in your page HTML and your secret key is not in your database.
  2. Enable Stripe Radar’s canned rules. “Block if card tested at multiple merchants in a short window” is free and very effective. Add 3DS challenges for low-dollar attempts.
Stripe Dashboard Radar > Rules tab showing the All, Request 3DS, Allow, Block, and Review counter chips with a list of enabled rules including Allow, Block, Block, Block, Block, and Request 3DS with their conditions
Stripe Radar’s canned rule library. Enable the Block and Request 3DS rules first.
  1. Put a bot challenge on your forms. Cloudflare Turnstile or Google reCAPTCHA on every checkout or payment form. Most WordPress payment plugins have first-class support for these. If not, Cloudflare Bot Management at the edge will catch most of it.
  2. Rate-limit payment endpoints at Cloudflare or in your WordPress firewall plugin. A single IP attempting 50 checkouts in a minute is never legitimate.
  3. Use restricted keys for everything that is not core payment processing. Zapier, Make, n8n, accounting integrations. All of them should use scoped rk_live_… keys, never sk_live_….
  4. Keep the inventory. Know every place a key lives. When something goes sideways, you can rotate confidently.
  5. Monitor Stripe Radar costs. The $0.02-per-review fee is small until it is not. A sudden spike in Radar reviews is the alert.

Frequently Asked Questions

Why is Stripe charging me for blocked transactions?

Stripe Radar reviews every payment attempt for fraud, including the ones it blocks. The fee is roughly $0.02 per review. Normally that is invisible because your real volume is the dominant cost. During a card testing run, the volume of blocked attempts can be 1,000x your normal traffic, which is what makes the bill jump.

Is my Stripe account compromised?

Not necessarily. If the attacker is only using your publishable key (pk_live_…), they are abusing your endpoint, not your account. Your account is “safe” in the technical sense, but you are paying the Radar review fees. If you see API calls from IPs you do not recognize using your secret key, that is a real compromise and you should roll keys immediately.

Should I disconnect Stripe Connect during an attack?

Only if you have evidence that the Connect credentials themselves are compromised. For a pure publishable-key attack, the fix is bot defenses and rate limits on your checkout, not disconnecting. If you do disconnect and reconnect, verify your webhooks afterward. That is where most sites get bitten.

Does Paid Memberships Pro have built-in protection against card testing?

Paid Memberships Pro integrates with Stripe via Stripe Connect by default, which means your secret key is not stored in your WordPress database and the checkout uses Stripe-hosted Checkout. That eliminates two of the three attack surfaces described in this post. You should still pair it with a bot challenge and rate limits at the edge to defend the third (direct publishable-key abuse).

How do I tell which key is being abused?

Go to Stripe Dashboard > Developers > API keys and check the last used IP for each key. Any key showing recent activity from an IP that is not your server is suspect. Cross-reference with Developers > Logs to see what API methods that key has been calling.

Final Recommendations

Card testing attacks are not going away. The fundamentals (Connect-based integration, Stripe-hosted Checkout, restricted keys, edge-level bot defenses, and an honest inventory of where your keys live) handle most of them quietly. When something gets through, the playbook is straightforward:

  1. Find which entry point is being abused (site checkout, publishable key, or secret key).
  2. Roll the affected keys with Expire now.
  3. Scan for malware if the secret key looks compromised.
  4. Disconnect and reconnect Stripe Connect to issue fresh credentials.
  5. Re-verify your webhooks. This is the step where most people get bitten. Do not skip it.

If you are running Paid Memberships Pro and have not yet switched your Stripe integration to Stripe Connect, do that next. It removes your secret key from your WordPress database and pushes the publishable key off your page HTML. That alone eliminates the two most common attack surfaces.



Was this article helpful?
YesNo