LOADING
PREYANSH
SHAH
WRITING
001
ARTICLE
JANUARY 28, 2026 PREYANSH SHAH

I Could See Your Invoices. All of Them. Forever.

An IDOR so simple it made me question everything I knew about software engineering. And humanity.

002

There are bugs that make you feel like a genius.

Then there are bugs that make you feel like the entire software industry needs to sit in a corner and think about what it’s done.

This one was the second kind.


Background: What Even Is IDOR

For the uninitiated — IDOR stands for Insecure Direct Object Reference.

In plain English: the server gives you a URL or an ID that points to something, and then forgets to check whether you’re actually allowed to see that thing.

It’s the digital equivalent of a hotel handing you a key card and then leaving all the other rooms unlocked. You asked for room 204. But nobody checked if you also walked into 311.

IDOR vulnerabilities are embarrassingly common. And the embarrassing part isn’t that developers make this mistake — it’s how confident the application feels when you exploit it. No errors. No warnings. Just: here’s the data you definitely shouldn’t have.


The Target

Let’s call it redacted-finance.com — a B2B invoicing and billing platform. The kind of thing small businesses use to send invoices, track payments, and store sensitive financial records.

The program had been open for a while. It had a decent scope. And it specifically mentioned that financial data exposure was in-scope and would be rewarded generously.

I noticed that last part.


Finding the Thread

I started by doing what I always do: just using the app like a normal person.

I created an account, set up a test workspace, and generated a few dummy invoices. Nothing fancy. Just getting a feel for how the application moved data around.

When I clicked on one of my invoices, the URL looked like this:

https://redacted-finance.com/invoices/view?id=100392

I stared at that id=100392 for a moment.

It was sequential. Numeric. Incrementing.

I changed it to 100391.

I saw someone else’s invoice.


Okay But How Bad Was It Really

At this point a less thorough hunter would’ve written the report and moved on. Valid bug, clean PoC, collect bounty, go home.

But I wanted to understand the blast radius. So I wrote a quick script — nothing fancy, just a loop — to request a range of invoice IDs and log the responses.

What came back was not great.

The invoices contained:

  • Full company legal names
  • Business addresses
  • VAT and tax registration numbers
  • Invoice line items (what they bought, from whom, for how much)
  • Payment status (paid, overdue, disputed)
  • Contact names and email addresses
  • Bank reference numbers on some older invoices

For a range of about 500 IDs I tested, roughly 340 returned valid invoices from real companies.

This wasn’t just an information disclosure. This was a full financial dossier on hundreds of businesses, accessible to anyone with a free account and a browser.

I closed the script. Made coffee. Sat with that for a minute.


It Gets Worse

Out of curiosity — and because I apparently enjoy suffering — I also checked the API endpoints directly.

The web UI had this endpoint:

GET /api/invoices/100392

Same issue. No auth check beyond “are you logged in.” Not “are you logged in and do you own this invoice.” Just: logged in? Here you go.

But the API also had a bulk endpoint:

GET /api/invoices?ids=100392,100393,100394

You could request up to fifty IDs in a single call.

I tested it. It returned all fifty. No ownership check. No rate limiting. Nothing.

So theoretically, you could extract the entire invoice database in bulk with a few thousand requests. At fifty invoices per call. With no rate limiting.

I did not do this. I had enough evidence. I closed the terminal and started writing.


The Report

Title: IDOR on Invoice Viewer Endpoint Allows Unauthenticated Access to Financial Records of Arbitrary Users

Severity: High (borderline Critical given the data sensitivity)

Impact: Any authenticated user can access the full invoice data of any other user or organization on the platform by enumerating the numeric invoice ID parameter. The exposed data includes company identity information, financial transaction details, tax identifiers, and contact information. The bulk API endpoint compounds this by allowing mass extraction.

Steps to Reproduce:

  1. Create a free account and log in
  2. Generate any invoice to obtain a reference ID
  3. Navigate to /invoices/view?id=[target_id] with any other numeric ID
  4. Observe full invoice data for another organization returned without error
  5. Repeat via the API endpoint /api/invoices/[id] for programmatic access
  6. Optionally use the bulk endpoint /api/invoices?ids=... with up to 50 IDs per request

I included a screen recording, HTTP logs, and a sanitized sample of three different organizations’ invoices I’d accessed (with personal details blurred).


Triage: Faster Than Expected

This one moved quickly.

Within six hours: triaged as High, escalated to engineering.

Within twenty-four hours: a partial fix was deployed. The single-invoice endpoint now correctly checked ownership. The bulk endpoint did not.

I pointed this out.

Another forty-eight hours: bulk endpoint fixed. Rate limiting added to both. Non-sequential IDs (UUIDs) deployed for all new invoices going forward.

They didn’t backfill existing invoices with UUIDs, which I noted in my follow-up. They acknowledged this as a known limitation and marked it as accepted risk given the now-present authorization checks.

Fair enough.

Bounty: $$$


Why This Happens (And Why It Keeps Happening)

IDOR isn’t a complicated bug. The fix isn’t complicated either.

Before returning any resource, check: does the authenticated user own this resource, or have explicit permission to view it?

One line of logic. Applied consistently.

But here’s why it keeps slipping through:

Developers build features fast. They add the auth check on the creation endpoint. They forget it on the retrieval endpoint. The tests pass because the tests use the same user to create and retrieve. Nobody tests cross-account access because that’s not in the happy path.

And then someone like me comes along with two browser tabs and five minutes of curiosity.

The scarier part? This application had been in production for years. This endpoint had existed, uncheckled, the whole time. Every invoice ever created was accessible to every other user for the entire lifetime of the product.

Nobody noticed. Or if they did, nobody said anything.


What I Took Away

Sequential IDs in URLs are a red flag, not a death sentence.

The real issue is the missing authorization check, not the ID format. But sequential IDs make the attack trivially easy to discover and exploit. UUIDs raise the bar meaningfully.

Test retrieval endpoints separately from creation endpoints.

Your auth check on POST doesn’t automatically protect your GET. These are different code paths. Test them independently.

Bulk endpoints deserve extra scrutiny.

Any endpoint that accepts multiple IDs in a single call multiplies the potential impact of an IDOR. If you find an IDOR, always check if there’s a bulk variant. There usually is.


Closing Thought

I reported this bug on a Tuesday.

By Friday, the engineering team had patched both endpoints, added rate limiting, migrated new invoices to UUIDs, and sent me a very professional thank-you message.

That’s actually a good response time for something this impactful.

The invoice data of hundreds of companies was exposed for years. And the fix took three days.

Security is strange like that.


Reported and fully remediated through the official bug bounty program. All testing was limited to invoices generated by my own test accounts plus a small sample for proof-of-concept, immediately deleted after capture. Domain redacted per responsible disclosure norms.

003
END
← BACK TO WRITING
I Could See Your Invoices. All of Them. Forever. READY TO PLAY