Skip to content
article

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

Nadia Benali@nullbyte_nadia

September 18, 2026 · 4 min read

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

15

Sign in to join the conversation.

Sign in to comment
Fatima Al-Sayed@fatima_appsec#8 in Hack The Planet
This is the cleanest explanation of authN vs authZ I have seen on here. I am stealing "being logged in as somebody is not the same as being allowed to read everybody" for our secure code review training. We find exactly this in the bank every quarter, usually the moment someone adds a new /export endpoint and forgets the tenant check.
6 net votes
Nadia Benali@nullbyte_nadia#1 in Hack The Planet
steal it, that is what it is for @fatima_appsec. the export endpoints are the worst, people treat them as "internal" and forget they still hang off the same auth.
4 net votes
Kabir Sethi@0xkabir#5 in Hack The Planet
idor gang 🫡 my first ever p1 was literally this, invoice id++ on a logistics panel. triager marked it high in 2 days though, u got the pain version. 94 days is insane for a confirmed data leak
5 net votes
Ankit Khurana@faang_or_bust1 rep in Hack The Planet
cool story but changing a number in a url is not hacking lol. real research is memory corruption, this is a crud bug
-13 net votes
Nadia Benali@nullbyte_nadia#1 in Hack The Planet
the "crud bug" leaked every customer on the platform and paid four figures. a heap overflow that crashes one box is cute, an authZ miss that reads the whole tenant table is the breach that ends up in the news. severity is about impact, not how clever the trick was.
7 net votes
Karl Lindqvist@kernel_karl#2 in Hack The Planet
He also has not sent a PR to anything, ever. Ignore him.
8 net votes
Karl Lindqvist@kernel_karl#2 in Hack The Planet
The "requires authentication" line is not ignorance, it is a liability script. Someone in legal told them to stop calling it a vulnerability. You did everything right and it still took a public clock to move them. This is why disclosure needs teeth.
11 net votes
Nadia Benali@nullbyte_nadia#1 in Hack The Planet
you are not wrong and it is depressing. the VP who appeared on day 52 had clearly been told "there is a deadline now". nothing moves a company like a date they did not choose.
2 net votes
Wolfgang Engel@dsgvo_wolf#6 in Hack The Planet
I would like to point out that under the GDPR this is very likely a reportable Breach. Company names, addresses, partial card data, accessible to any authenticated user: that is unauthorised access to personal data within the meaning of Article 4. If they are in the EU they had 72 hours from becoming aware, not 94 days to patch. Did they notify?
12 net votes
Nadia Benali@nullbyte_nadia#1 in Hack The Planet
I asked them exactly this @dsgvo_wolf and got a non answer. "we assessed the risk as low". low risk to whom. I reported it, I cannot make them file. but you are right that the clock for them is not the same as the clock for me.
2 net votes
Hannah Weiss@hannahweiss1 rep in Hack The Planet
The single account testing blind spot is universal. In hospital integrations we call it the "it works for patient zero" problem. One test patient, every permission in the world, and the authorisation gaps are invisible until a second, less privileged account exists. Your three line repro is a good artefact, I may show it to a vendor of ours who keeps saying "cannot reproduce".
9 net votes
Arash Karimi@arash_403#7 in Hack The Planet
Reading this from a place where the dashboard would also leak your data to the government, so, two problems. But the engineering point stands and is clean. Scope the query, do not filter after. I put this on the wall.
8 net votes
Ray Alvarez@ruck_n_code1 rep in Hack The Planet
saving this. getting out of the army soon and learning this stuff for the transition, this is the kind of real report the courses never show you. the timeline is the scary part. roger on test what you own, copy that loud and clear
6 net votes
Daniela Castaño@dani_ctf1 rep in Hack The Planet
Uy, el clásico "requiere autenticación por lo tanto es seguro". En el banco veo esto todo el tiempo, usted inicia sesión y de repente puede ver todo. Gracias por el writeup, lo voy a compartir con mi equipo (traducido jaja).
7 net votes
Achieng Owino@achieng_devrel1 rep in Hack The Planet
The 94 days is the real story. We build APIs for a living and the number of teams who think "auth middleware is on, we are done" is terrifying. Per tenant scoping in the query, every time, no exceptions. Sharing this with our platform team.
6 net votes