the invoice IDOR that paid for my holiday, and the 94 days it took to fix
one number in a URL, a very long email thread, and a vendor who really did not want to hear it
Contents
this is a writeup of a bug I reported to a SaaS billing vendor I am calling "Facturo" here (not the real name, the program is private and I am still under their disclosure terms). nothing in this post lets you attack them or anyone else. it is a plain IDOR, the oldest bug in the book, and the only reason it is interesting is the 94 days it took them to admit it.
the setup
Facturo is a billing dashboard that a few hundred small companies use to send invoices. I had two accounts, both mine, both paid for out of my own pocket (this matters, test what you own). when you open an invoice the dashboard calls their api:
GET /api/v2/invoices/48213 HTTP/2 Host: app.facturo.example Authorization: Bearer [REDACTED, my account A token] Cookie: [REDACTED]
48213 is just a number that goes up. no uuid, no per tenant prefix, nothing. so I logged in as account B, grabbed a fresh token, and asked for account A's invoice id:
GET /api/v2/invoices/48213 HTTP/2
Host: app.facturo.example
Authorization: Bearer [REDACTED, my account B token]
HTTP/2 200
{"id":48213,"customer":"[account A, a company that is me]","total":"812.40", ...}
account B, which has no relationship to account A at all, got account A's invoice back. the server checked that my token was valid. it never checked that the invoice belonged to me. that is the whole bug. with a valid login you could walk the ids and read every invoice on the platform: company names, amounts, addresses, in some cases the last 4 of a card. I did NOT do that. I proved it with two ids I owned, screenshotted the two responses side by side, redacted everything, and stopped.
the timeline
- day 0 report sent through their form. severity high, clear repro, two accounts both mine, offer to hop on a call.
- day 4 auto reply: "thanks, we take security seriously". the sentence that means nobody has read it.
- day 11 a support agent asks me to "confirm the issue persists" and sends me a password reset link. I do not need a password reset. I need an engineer.
- day 23 "our team could not reproduce". they were testing with one account. an IDOR is invisible if you only ever look at your own data.
- day 24 I send a 3 step repro a child could follow, two accounts, two tokens, one id.
- day 51 silence. I send a polite nudge and mention that my 90 day clock is public and started on day 0.
- day 52 suddenly a VP emails me. funny how that works.
- day 94 fixed. the api now checks the invoice owner against the token. they added a per tenant id too. bounty paid, and it was generous, I will give them that.
what the vendor said
my favourite line from the thread, which I have permission to paraphrase: "we did not consider this a vulnerability because the endpoint requires authentication." authentication is not authorisation. being logged in as somebody is not the same as being allowed to read everybody. this is the single most common misunderstanding I see, in startups and in banks, and it is why access control is number one on the OWASP top 10.
how to not ship this
- every object fetch answers two questions, not one: who are you (authN) and are you allowed this specific row (authZ). the second one is the one people forget.
- scope the query, do not filter after.
WHERE id = ? AND tenant_id = ?in the query itself. never fetch by id then check ownership in app code, someone will delete that check in a refactor. - sequential integer ids are not a vuln by themselves, but they make walking trivial. uuids are not a fix for missing authZ, they are a speed bump, do both.
- test with two accounts. always. your single happy path account will never find this.
bref. one number in a url, 94 days, and a holiday in the Alps. report responsibly, redact everything, and do not touch data that is not yours even when the door is wide open. especially then.
Written by
Nadia Benali
security researcher. bug bounty since 2014, hall of fame on more programs than I can count. web, auth flows, OAuth misconfigs. ex-pentester. Lyon 🇫🇷 DMs open, but not for hacking your ex's instagram.
Get the best of #hack_the_planet weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to comment