DEV Community

Posting Dude
Posting Dude

Posted on

Fake Signups and Card Testing: A Small Defense Stack for a Tiny SaaS

If your signup form has been live for a few months, go look at your newest users table right now. Sort by created_at, scroll a bit. Most of us find the same stuff: names like xkqjvbtr, emails at domains that don't exist, five accounts from the same IP in a minute, and nobody ever logging in again.

It's easy to treat that as noise. It isn't, for two reasons. It quietly wrecks your numbers, and if there's a card form anywhere behind it, it can turn into card testing, which costs actual money and trust with your payment processor.

Here's the small defense setup I'd put in place on a tiny SaaS, roughly in order of effort. None of it needs a security team.

First, know what you're looking at

There are usually three different things mixed together:

  1. Junk signups. Scripts filling every form they find, often to drop spam links into your app or just because your form is there. Annoying, mostly harmless, but they pollute everything.
  2. Real people abusing a free tier. One person, ten throwaway emails, ten free trials. Different problem, different fix (I'm not covering it here).
  3. Card testing. Someone with a list of stolen card numbers uses your checkout or "add payment method" form to check which cards still work. Stripe's guide on card testing explains this well, and one detail stuck with me: testers like card setup flows because a validation there usually doesn't show up on the cardholder's statement, so nobody notices.

The third one is the one to take seriously. Stripe lists what it does to you: disputes from the payments that went through, a higher decline rate that can make your real customers' cards get declined more, extra fees, and fake "revenue" that looks like new customers in your data.

How to tell you're being hit

Open your Stripe dashboard and look for:

  • a spike in failed or blocked payments
  • a pile of 402 errors in the developer logs, lots of them generic_decline
  • small amounts from customers with nonsense names and emails

For junk signups, a quick query does the job:

SELECT date_trunc('day', created_at) AS day,
       count(*) AS signups,
       count(*) FILTER (WHERE last_login_at IS NULL) AS never_logged_in
FROM users
WHERE created_at > now() - interval '30 days'
GROUP BY 1
ORDER BY 1;
Enter fullscreen mode Exit fullscreen mode

If never_logged_in suddenly jumps on some days while real traffic didn't change, that's not a viral moment.

The defense stack, cheapest first

1. Put a CAPTCHA on signup and verify it on the server

I like Cloudflare Turnstile because it's mostly invisible to normal people, but the specific product matters less than one rule: check the token on your backend. The widget alone protects nothing. Cloudflare's docs say it plainly: tokens can be forged, they expire after 5 minutes, and each one can only be validated once. So your signup handler should call the verify endpoint and reject the request if it fails:

async function verifyTurnstile(token, ip) {
  const res = await fetch(
    "https://challenges.cloudflare.com/turnstile/v0/siteverify",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        secret: process.env.TURNSTILE_SECRET_KEY,
        response: token,
        remoteip: ip,
      }),
    }
  );
  const data = await res.json();
  return data.success === true;
}
Enter fullscreen mode Exit fullscreen mode

Plenty of indie apps render the widget and never check the token. Bots just skip it.

2. Rate limit the endpoints that matter

Not the whole site. Just signup, login, password reset, and anything that touches a card. Something like "max 5 signups per IP per hour" stops most scripts and almost no humans. Stripe suggests the same idea for card testing, for example limiting how many new customers one IP can create in a day.

If you're on a framework with middleware, this is maybe 20 lines with a Redis counter, or a rule in your CDN/WAF if you already use one.

3. Don't let strangers reach the card form

This is the one I'd do first if there's a card form anywhere. If someone can hit "add a card" without an account, or with an account they made two seconds ago, you're a free card checker. Make people sign up and verify their email first, then show the payment step. Stripe's guide calls this out too: the easier it is to reach your payment form, the easier card testing gets.

Also limit how many cards one account can add. A real customer adds one, maybe two. Nobody legit adds nine in ten minutes.

4. Use the hosted payment pieces if you can

If you're on Stripe, Checkout or the Payment Element come with card testing protection built in (rate limits, models, CAPTCHA triggers). If you rolled your own card form against the raw API, you're carrying more of that work yourself. For a small team, letting Stripe own that part is usually the right call.

5. Be careful with retries

Stripe's docs had one I didn't expect: aggressive payment retries can look like card testing if they come in spikes with a low success rate. And after an attack, don't keep retrying cards attached to the fake customers, because that just repeats the attack from your side.

Clean up after an attack

If something already got through:

  • refund the junk payments quickly, before they turn into disputes
  • delete or flag the fake accounts so they stop showing up in your numbers
  • check whether your secret key could have leaked (it should never be in front-end code or a public repo)
  • watch the dashboard for a few days to make sure the fixes worked

The part people miss: your numbers

Even if no money moves, fake signups mess up your decisions. Your signup-to-activation rate drops, you start "fixing" onboarding that was fine, and a channel looks great when it was really a bot farm. When you look at the few product metrics that matter before your first 100 users, filter out accounts that never logged in first, or the activation number is just lying to you.

Fake emails also bounce, and bounces hurt your sender reputation. Then your real users' password resets and welcome emails start landing in spam. I wrote about why your password reset email is probably in spam here on DEV, and junk signups are one of the quiet causes.

So: an afternoon of work. Server-side CAPTCHA check, rate limits on four endpoints, card form behind a verified account. That covers most of what hits a small product.

Have you been hit by card testing yet? What tipped you off?

Top comments (0)