IDOR in PDF and Invoice Exports: The Overlooked Endpoint
The main app might have flawless authorization on every JSON API, and then there's /invoices/48213/download.pdf — an export endpoint someone wrote in an afternoon, wired straight to a database ID, and never gave the same access checks as the rest of the app. Export routes are where IDOR hides, because they feel like plumbing rather than features.
Why exports rot
Export and reporting endpoints tend to be second-class:
- They're generated by a separate service or a background worker that trusts whatever ID it's handed
- They return a file, so developers test "does the PDF render" and forget "can I render someone else's PDF"
- IDs are usually sequential database keys (
invoice_id,report_id,document_id), not random tokens - They're often cached or served from object storage, sometimes without re-checking the requester
Any of those alone is a smell. Together they're an open door.
The two-account test
You need account A and account B, both yours. Log in as A, generate an invoice or report, and capture the download request:
GET /billing/invoices/100245/export.pdf
Cookie: session=<account A>
Now, still authenticated as B, request A's document ID. If B receives A's invoice, that's a clean IDOR — a real access-control failure with your own two accounts proving it, no guessing about who owns what.
Then probe the ID space. Decrement and increment the number. If 100244 and 100246 return other customers' invoices, the endpoint has no per-object authorization at all. Stop enumerating the moment you confirm it — you do not need to pull a hundred strangers' invoices to prove the bug, and doing so turns research into a data breach you caused.
The subtle variants
A few things that separate a good report from a dupe:
- Unauthenticated access. Try the export URL with no session. Some file endpoints, especially those redirecting to signed storage URLs, skip auth entirely.
- Guessable "random" IDs. A UUID isn't automatically safe. If it's a v1 UUID (timestamp-based) or sequential internal ID wrapped in base64, it's still predictable. Decode before assuming randomness.
- Signed storage URLs with no expiry or weak signing. The export may 302 you to an S3/GCS/Azure link. Check whether that link is scoped and expiring, or a permanent handle anyone can replay.
- Format and parameter swaps.
?format=pdfvs?format=csv, or/printvs/download, sometimes hit different code paths with different (or missing) checks.
Invoices leak more than money
Spell out impact in the report, because invoice PDFs are dense with PII: full names, home addresses, partial card numbers, line items revealing what a customer bought, tax IDs, sometimes phone numbers. An IDOR here isn't "download a file" — it's bulk PII exposure across the customer base, and that framing drives severity.
Reporting cleanly
Include both accounts' context, the request that fetched the other account's document, the ID pattern you observed, and a single cross-account example (redact the victim's data — proving you could read it is enough). If unauthenticated access works, lead with that; it's the worst case.
Recommend enforcing object-level authorization on every export path (check the requester owns or may access the object, server-side, every time), using unpredictable identifiers, and — if serving via signed storage URLs — scoping them tightly and expiring them fast.
OWASP's IDOR Prevention Cheat Sheet covers object-level access control patterns. Point at the specific control the export endpoint skipped; "PDF route lacks the authorization the JSON API already enforces" is a tidy root cause.