Post

It's 3 AM and I'm Creating a Thousand Invoices

It's 3 AM and I'm Creating a Thousand Invoices

I wasn’t supposed to be awake at 3 AM. But I’d had too much coffee and my brain wouldn’t shut off, so I figured I’d do one more test before bed.

There’s something meditative about curling endpoints at night. The house is quiet. The terminal glows. You send a request and wait for the universe to answer.

Tonight, the universe had opinions.

The Endpoint That Shouldn’t Exist

I’d found this endpoint earlier in the week. It was meant for authenticated users to request duplicate awards — like if you won something and needed another copy of the trophy. Fair enough.

But when I looked at the controller code, I noticed something odd. No @Authorized() decorator. No authentication check at all. Just a straight path from HTTP request to database transaction.

So I sent a test request. Just to see.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -s -X POST "$API/api/duplicate-trophy" \
  -H "Content-Type: application/json" \
  -d '{
    "category_ids": [440],
    "productions": ["Test Production"],
    "quantities": [1],
    "name": "Test",
    "email": "[email protected]",
    "company": "Test Co",
    "address": "123 Test St",
    "city": "Testville",
    "state": "CA",
    "zip": "12345",
    "country": "US",
    "phone": "555-1234"
  }'

HTTP 201. Created.

“Huh,” I thought. “That’s probably just returning a success message. Surely it’s not actually…”

I logged into the accounting system. There it was. Invoice #2611. For one trophy. $670.

Sent by an unauthenticated curl request from my laptop at 2:47 AM.

The Rabbit Hole

Okay, so unauthenticated invoice creation is bad. But how bad? I started poking.

First question: What happens if I send a lot of requests?

I wrote a quick loop:

1
2
3
4
5
for i in {1..10}; do
  curl -s -X POST "$API/api/duplicate-trophy" \
    -d "{... quantities: [1] ...}" &
done
wait

Ten requests. Ten invoices. All created within seconds of each other. No rate limiting. No “are you sure?” dialog. Just invoice after invoice after invoice.

I felt that familiar pit in my stomach. The one that says “this is worse than you thought.”

Scientific Notation at 3 AM

Here’s where things get weird.

I was looking at the quantities field. It’s an array of numbers. In JSON, that means it accepts… well, numbers. And JavaScript has opinions about what constitutes a number.

At 3:15 AM, half-delirious from caffeine and curiosity, I tried something:

1
2
3
4
5
6
curl -s -X POST "$API/api/duplicate-trophy" \
  -d '{
    ...
    "quantities": [1e3]
    ...
  }'

1e3. Scientific notation for 1000.

The response came back with a total amount of $730,000.

I stared at the screen. I checked the accounting system. There it was. Invoice #2620. For 1000 trophies. $730,000.

I tried 1e6 just to see what would happen. The system accepted it. I didn’t let it finish calculating because I didn’t want to be responsible for a $730 million invoice, even in staging.

The Other Weirdness

While I was there, I tested a few other things:

Floats? Sure, why not.

1
"quantities": [1.5]

Created an invoice for 1.5 trophies. Because apparently you can sell half a trophy.

Zero? Yep.

1
"quantities": [0]

$0 invoice. Which… why? Why would you create a $0 invoice?

Negative numbers? This one actually failed, but in an interesting way. It returned null values for the amount and balance. Still created something in the database, though.

The Response I Sent at 3:47 AM

I sat there in the dark, looking at my terminal, and composed a Slack message to the team. I debated waiting until morning. Professionalism. Business hours. All that.

But then I thought about what happens if someone finds this before we fix it. Someone with less ethical restraint than me. Someone who might actually create that $730 million invoice in production.

So I sent it. With a lot of “URGENT” and “SORRY FOR THE LATE MESSAGE” and “THIS IS BAD.”

What I Learned (Again)

I’ve been doing this DevOps/DevSecOps thing for a while now, and I keep learning the same lessons:

  1. If an endpoint doesn’t explicitly check authentication, it’s public. Even if you “intended” it to be internal. The internet doesn’t care about your intentions.

  2. IsNumber() is not validation. It’s a type check. 1e3, 1.5, -999999 — these are all numbers. Validation means asking “does this make business sense?”

  3. Race conditions are real. Ten parallel requests creating ten invoices with no locking mechanism. That’s not a security bug, but it’s a reliability nightmare.

  4. The edge cases will find you. At 3 AM. When you’re tired. And they’ll be weirder than you imagined.

The Fix (That Came the Next Day)

To their credit, the team jumped on this fast. By 10 AM they’d implemented:

1
2
3
4
5
@Post('/duplicate-trophy')
@Authorized()  // <-- Added this
async createDuplicateTrophy(@Body() dto: CreateDuplicateTrophyDto) {
  // ...
}

And for the validation:

1
2
3
4
5
6
class CreateDuplicateTrophyDto {
  @IsInt({ each: true })
  @Min(1, { each: true })
  @Max(10, { each: true })
  quantities: number[];
}

IsInt rejects scientific notation and floats. The @Min and @Max enforce business logic. Problem solved.

Why I Do This

People ask me sometimes why I spend my nights breaking things. Why I read CVE reports at 2 AM. Why I can’t just trust that the code works.

Because I’ve seen what happens when you don’t check. I’ve cleaned up breaches. I’ve had the 4 AM calls. I’ve had to tell a CEO that yes, someone really did create 10,000 fake invoices.

It’s easier to find these things yourself, in staging, at 3 AM, with a cup of cold coffee. Than to find them in production because someone else found them first.

So I’ll keep curling endpoints in the dark. Keep wondering “what if I try this?” Keep sending those late-night Slack messages.

Because someone has to. And I guess that’s me now.


If you’re reading this at 3 AM wondering about your own APIs, maybe send that curl request. See what happens. Just — use staging, okay?

Find me on LinkedIn or GitHub. I promise I check messages during daylight hours too.

This post is Copyrighted by the author.