Guessable Tokens on Private Uploads
The image URL looked random. Part of it wasn't — and that part was enough to walk into other people's uploads.
Target anonymized · an online photo-printing service
A short one to finish, and a common one: a media endpoint that leaned entirely on its URLs looking unguessable, when part of them wasn't.
The spark
Images were served from a multi-segment path. Most of it looked random — but one segment was a plain, incrementing number. A single predictable component in an otherwise "random" identifier is usually the whole ballgame.
Digging in
I ran the gate: a fabricated path returned an error, so the endpoint genuinely resolved the identifier rather than ignoring it. Then I took a real path and nudged the sequential segment to a neighbouring value. Back came a different user's uploaded image — no login, no ownership check.
The other segments were window dressing. The security of the whole URL collapsed to the one part an attacker could count.
The exploit
GET /i/{fixed}/{fixed}/{sequential±1}/image HTTP/2
Host: img.example.com
# no auth — returns another user's upload
Iterate the predictable segment and you scrape strangers' private uploads.
Impact
Unauthorised disclosure of private, user-uploaded images — the personal photos people trusted the service to keep to themselves.
The fix
Require authorization on media access, and make resource references fully unguessable and access-controlled — the unguessability is a convenience, the ownership check is the control.
All identifiers, targets and payloads in this post are anonymized or defanged. Findings were reported and resolved through responsible disclosure.