How I Stopped a Credit Card Thief From Ripping Off 3,537 People – and Saved Our Nonprofit in the Process

It was 9:47am on an unassuming Tuesday when I logged into our Stripe account to do some routine analysis. The dashboard loaded, and I nearly spit out my coffee. The data showed that over the past week, a single malicious actor had made over 20,000 donation attempts using stolen credit cards.

Thankfully, Stripe‘s built-in fraud detection had blocked the vast majority of those transactions. But 3,537 of them, totaling over $280,000, had slipped through before the algorithm caught on. That meant thousands of people were about to see fraudulent charges from our nonprofit on their statements.

My heart raced as I tried to process the scale of the attack. Who was this thief, and how did they get their hands on so many credit card numbers? What kind of sophisticated techniques did they use to evade our security measures? And most pressingly – how could I clean up this mess before it dealt a fatal blow to our org and its mission?

Anatomy of a Donation Heist

As the lead developer for our nonprofit‘s website and donation platform, my mind immediately started dissecting the attack. Our setup was pretty standard for a small-to-midsize charitable org:

  • A simple donation form built with HTML, CSS, and JavaScript
  • Donor and transaction data stored in a PostgreSQL database
  • Credit card processing handled via Stripe‘s API and checkout flows
  • Server-side code written in Python using the Django framework
  • Hosting on AWS, with the usual suite of security best practices applied

We were fully PCI compliant and had invested heavily in cybersecurity, knowing that nonprofits are juicy targets for fraudsters. We encrypted all traffic with SSL, used strong password policies and two-factor authentication, and kept our systems patched and malware-free.

So how did the thief pull off this heist? After some frantic digging, I discovered the likely culprit: a flaw in how our form handled validation for the CVV field.

Most card-not-present fraud detection systems rely heavily on checking the Card Verification Value – those 3-4 digits printed on your physical credit card but not stored in merchant databases. If a fraudster has your card number and billing address but not your CVV, there‘s a good chance it‘s a stolen card.

Our donation form was configured to require a valid CVV, but I found that the thief had managed to bypass that check by manipulating the HTTP headers on the POST request. They then hammered our API endpoint with a barrage of stolen card numbers, tweaking the CVV until they found ones that our system (and Stripe‘s) would accept.

In hindsight, it was a clever exploit – one that I imagine has snared many other websites over the years. But in that moment, all I could think about was the 3,500+ victims who would soon be wondering why our org had stolen their money.

The $53,000 Chargeback Nightmare

Nonprofits like ours rely on the generosity and trust of our donors to survive. Having to inform them that we inadvertently facilitated credit card fraud would be devastating to that trust. But the financial implications were even more existential.

You see, whenever a cardholder disputes a credit card transaction as fraudulent, the card issuer initiates a "chargeback" – essentially a forced refund where the merchant has to give back the disputed funds. On top of that, the merchant gets hit with a penalty fee for each chargeback, which for Stripe is a whopping $15.

I did some quick, nauseating math. At $15 per transaction, 3,537 chargebacks would cost us $53,055 in fees alone. Never mind the $280K in lost donation revenue – the fees by themselves would completely deplete our nonprofit‘s rainy day fund.

If the bad actor succeeded in pushing through those chargebacks before I could refund the victims, there was a very real chance it could bankrupt us. And as the person responsible for keeping our donation platform secure, that prospect made me want to vomit.

The Race to Refund

I took a deep breath, cracked my knuckles, and got to work on damage control. Job one was to patch the CVV validation hole so the attacker couldn‘t slip any more fake donations through. I had our dev team whip up a fix and push it live within minutes.

Step two was to refund every single one of those 3,537 fraudulent transactions before the victims filed chargebacks. Here‘s where my choice of payment processor proved crucial.

Stripe provides a robust API for issuing refunds programmatically, either one at a time or in batches. My first instinct was to write a script that would automatically look up each transaction ID and fire off a refund request.

But as I feverishly pored through the API docs, a nagging voice in the back of my head gave me pause. What if the script had a bug or missed an edge case? At this scale, even a 0.1% error rate could mean several chargebacks slipping through. And based on my back-of-the-envelope math, we could only absorb about 200 chargebacks before exhausting our Stripe balance.

In a moment of decidedly un-automated clarity, I resolved to start refunding the charges manually, one by painstaking one, until I could be 100% confident in the script. It would be mind-numbing. It would invite carpal tunnel. But it was the only surefire way to minimize our chargeback exposure.

For the next several hours, I transformed into a refund robot – search for transaction, click "Refund", click "Confirm", wait for the "Success!" message, repeat ad nauseam. I barely registered the world around me, laser-focused on the singular goal of returning every ill-gotten dollar to its rightful owner.

Hitting the Refund Limit

Two hours and one very fatigued mouse hand later, I had wrangled about a third of the fraudulent charges into the "Refunded" column. But as I went to process the next one, Stripe‘s UI flashed an unfamiliar error message:

"Refund failed: Cannot refund a transaction with insufficient funds in your Stripe account."

Huh? I thought Stripe would simply draw from our bank account if our Stripe balance couldn‘t cover the refunds. Then it hit me like a ton of bricks.

Part of Stripe‘s standard operating procedure is to initiate an automatic "payout" every night, transferring any available funds from your Stripe balance to your connected bank account. It usually takes a couple days for those payouts to clear.

In my single-minded rush to refund, I had completely forgotten about that daily sweep. So while our bank account now had an extra $280,000 in limbo, those funds were untouchable until the payout completed. Our Stripe balance was empty, and I was dead in the water on refunds.

Thus began the most harrowing leg of this nightmare relay race: trying to convince Stripe to make an exception and accelerate the payout so I could resume refunding. I must have exchanged 50 emails and phone calls with their support team that day, pleading my case to anyone who would listen.

"You don‘t understand," I implored the poor rep on the other end of the line. "Every minute we wait is another minute the crooks could file chargebacks and bleed us dry. Is there anyone there who can override the system just this once, given the circumstances?"

After several escalations and heart-pounding hold music interludes, I finally got a tentative response. "Okay, our risk team is willing to extend you a temporary negative balance allowance so you can finish processing refunds immediately. We‘ll just need you to initiate a wire transfer to bring your Stripe balance back to zero once your bank deposit clears."

I breathed the biggest sigh of relief of my professional career. They had given me the green light to resume my refund marathon, and I sprinted through to the finish line. By 1am, every last penny of the $280,000 was back in the victims‘ accounts where it belonged.

Lessons from the Trenches

As the dust settled on this wild ride, I reflected on how close we had come to catastrophe – and what I would do differently to avert it in the future. Here are some of the key lessons I‘m carrying forward:

  1. Disable automatic payouts in your payment processor. As tempting as it is to get your money as quickly as possible, it‘s crucial to maintain enough working capital in your processing account to handle unforeseen refunds or disputes. I‘ve since switched us to weekly manual payouts.

  2. Don‘t skimp on the unit tests for your donation forms. It‘s easy to test the happy path where everything works as expected, but ironically, fraudsters are some of the most rigorous QA testers out there. They will poke and prod at every nook and cranny of your validation logic, so you need to be just as thorough.

  3. When it comes to large-scale cleanup, automate at your own risk. It‘s hard to resist the allure of one-click fixes when you‘re facing down a mountain of toil, but sometimes the slower, manual approach is the safer bet – especially when the stakes are high. Scripts can miss edge cases; humans can adapt.

  4. Establish an incident response plan before you need it. The worst time to figure out your fraud response playbook is in the heat of an attack. Have those lines of communication and decision trees mapped out in advance, so you can focus on execution when the adrenaline is pumping.

  5. Treat your payment providers as true partners. Stripe wasn‘t just some disinterested vendor in this debacle; they were an extension of our team, with goals and incentives aligned with ours. By treating them as allies and being transparent about our needs, we were able to collaborate on a solution.

Beyond the tactical lessons, this experience also underscored for me the unique risks and responsibilities that come with running a nonprofit in the digital age. We are entrusted with not just our donors‘ money, but their personal data and their belief in our mission. Failing to protect those things can undermine the very purpose of our work.

At the same time, we can‘t allow ourselves to become so paralyzed by the fear of breaches that we make our donation process insurmountable. We have to strike a delicate balance between security and accessibility, so that giving remains as frictionless as possible for the generous people who power our cause.

It‘s a tightrope walk, to be sure. But if we as nonprofit technologists can stay nimble, vigilant, and resilient in the face of ever-evolving threats, I believe we can keep our orgs thriving – not just surviving. Because the world needs the good we put into it, now more than ever.

Similar Posts