Stealing Credit Cards on Reverb.com with Unscoped Finds
This is the story of how a fixation on computer security would not let go of me until I felt “good” at it. Imposter syndrome be damned, I was going to find a security bug and I was eventually going to tell this story.
Let me set the stage. It was 2016. Pre-pandemic. I was working as an infrastructure engineer for Reverb. Open office. A sensory nightmare – fluorescent lights, constant noise, no walls, nowhere to hide. But somehow I found a way to function in it. I’d spend hours with headphones on, tracing request parameters through controller actions while people had standups three feet from my desk. The kind of deep, paranoid focus this work requires is hard enough in a quiet room. Doing it in an open office while your nervous system is screaming at you to leave is something else entirely.
But that’s where I found it. A bug in the credit card controller that would let any authenticated user read, edit, update, destroy, or set as default any other user’s credit card on the entire platform.
I had just tested it in staging and reverted it. I knew it was going to work. Confidently, I walked into the CEO’s office and told him I thought we needed to invest in a security engineer. He smiled and said “Why?” So I sat down at his computer, pulled up the staging environment, and took his credit card with a specially crafted curl request. That was the end of that conversation and the start of my security career.
I can’t tell this story without crediting the engineer I looped in to help test and patch the bug. Every time I worked with this person they were amazing, and this time proved no different – within 30 minutes of demoing the vulnerability to the bosses, the fix was written, reviewed, and merged.
Since we were early adopters of infrastructure as code, we also had a snazzy continuous deployment pipeline that allowed us to ensure this was patched quickly. Additionally, we performed triage and incident response and were unable to determine that this was ever abused for the life of the code. All thanks to the OG reverb engineering team who pitched in to address this promptly.
The branch that fixed it was called do-not-steal-credit-cards. That’s not a joke. That’s the actual branch name. I still randomly grin about it from time to time when I remember.
I’ve been sitting on this story for close to ten years now. You see, as a security engineer, you learn things you can’t share. You begin to gain an understanding of the world around you and how leaky it really is. I’m still paranoid that by talking about this story I could affect former teammates unknowingly, but I’m confident we squashed all forms of these bugs before I moved on to Etsy.
The Bug
The vulnerability was an unscoped find – one of the most common and most dangerous patterns in Rails applications. The credit card controller was doing this:
credit_card: CreditCard.find(params[:credit_card_id])
That’s it. That single line. I found it using a single grep pattern – grep -r '.find(params\[' app/controllers/ – because that meant user input was going directly into a find. CreditCard.find searches the entire credit_cards table by primary key. There’s no scoping to the current user. If you know (or can guess, or can enumerate) a credit card ID, you can pass it as a parameter and the application will happily hand it to you. Edit it. Delete it. Set it as your default. Whatever the controller action allows. This was pre-CodeQL, pre-Semgrep, pre-LLM assisted vulnerability discovery. Good ole grep and the curiosity to look.
The fix was to scope the query through the association:
def safe_credit_card
current_user.credit_cards.find(params[:credit_card_id])
end
Now find only searches within cards belonging to current_user. If you pass someone else’s card ID, you get a 404 instead of their financial data.
It’s also worth noting: in 2026 you shouldn’t be passing sequential integer IDs around in requests at all. Use UUIDs or another hard-to-guess identifier. Sequential IDs are an invitation to enumerate – if my credit card is 1024, I’m going to try 1025. A UUID like a3b8f7d2-4e1a-4c9f-b6e3-9d2f1a8c5e7b doesn’t give an attacker anything to work with. Scoping your queries is still essential, but removing guessable identifiers from the equation is defense in depth.
This affected multiple controller actions – edit_credit_card, update_credit_card, destroy, set_default – all across the dashboard and the direct checkout flow. Here’s the PR that fixed it:

And the diff showing the actual fix across the controller:

Why This Happens
Unscoped finds are everywhere. They happen because Model.find(params[:id]) is the first thing you learn in every Rails tutorial. It’s in the scaffold generator. It’s the default. And it works fine until your application has more than one user who shouldn’t be able to access each other’s data – which is every application.
This is the difference between authentication and authorization, and it trips people up constantly. Authentication is “who are you?” – you logged in, great, we know you’re Adam. Authorization is “what are you allowed to do?” – just because you’re logged in doesn’t mean you should be able to access every record in the database. Reverb knew who I was. It just never bothered to ask whether that credit card was mine.
The pattern is an instance of IDOR (Insecure Direct Object Reference), and it shows up on the OWASP Top 10 under Broken Access Control. You’d think it would be rare in production at major companies. It is not. I’ve found it at multiple organizations across my career. It’s the kind of bug that passes code review because the line looks normal. It looks correct. You have to think adversarially to see the problem, and most developers aren’t thinking about what happens when someone changes a number in a request.
If you are developing code, working with Claude, publishing any software in any capacity that collects or stores someone else’s information – here’s your free security consultation from someone who has mentored many engineers: try to rob yourself. Seriously. Put on your little hacker hoodie, dim the lights if it helps, and see if you can steal the data you’re collecting. Change an ID in a request. Poke at an endpoint you shouldn’t have access to. You’ll feel silly for about five minutes and then you’ll never look at your code the same way again.
And yes – before you write to me telling me that “stealing” is not what happened here, you may be technically correct (the best kind of correct). We didn’t steal anything. We found a door without a lock, added a lock, and told this story so you’d go check your own doors. But I used the word “stealing” on purpose, because that’s the mindset that found this bug. Not “accessing.” Not “viewing.” Stealing. If you build anything that touches other people’s data – and in 2026, that’s most of us – you need to be thinking about what someone with bad intentions will try to do with it. That thinking needs to be in the design, not bolted on after the fact.
Why I Couldn’t Stop Looking
I found this bug because my brain wouldn’t let it go. I will look at a request parameter for hours. I will enumerate every possible value. I will follow the code path from the controller through the model to the database and back. I will do this at 3am without having decided to, on a weeknight, having forgotten to eat. And I will find the unscoped find that everyone else’s eyes slid past in code review.
If you’ve ever felt like you can’t think about anything but HTTP, like your brain picked a lock and refuses to put the lockpick down – I get it. That’s how I found this bug. That’s how I find all of them.
And that’s why I started this blog – to help understand how my autism influences my security work.
More to come.