DEV Community

Serguey Asael Shinder
Serguey Asael Shinder

Posted on

Everyone Checks the Login: Nobody Checks the Record

Most applications are careful
about who you are.

There is a login,
a session, a token,
middleware that runs
before anything else,
and a test that proves
an anonymous request is refused.

That work is usually good.

Then the request arrives
for /invoices/4193
and the code loads invoice 4193
and returns it.

You are logged in.

You are just not
the person that invoice belongs to.

Authentication asks
whether you are someone.

Authorisation asks
whether you are allowed
to touch this particular row.

The first is a door.

The second is every room
behind it,
and it is the one
people forget to lock.

The failure is boring
and that is why it survives.

Nobody wrote code
to leak other people's data.

Somebody wrote a query
that selects by id,
because that is the obvious query,
and the ownership check
lived in the page
that generated the link.

Hide the button
and the route stays open.

There is only one reliable habit here.

Put the ownership in the query.

Not in a guard clause above it,
not in the template,
not in the front end.

Where clause,
user id or tenant id,
every time,
so that a request
for a record you do not own
does not return a forbidden page.

It returns nothing,
because nothing matched.

Then go and try it.

Make two accounts.

Log in as the second one
and ask for the first one's ids.

Do it for every route
that takes an identifier,
including the ones
that only ever get called
by your own front end,
because your front end
is not the only thing
that can call them.

Watch the ids as well.

Sequential integers
tell a stranger
how many customers you have
and exactly what to ask for next.

That is not the vulnerability,
but it is the map to it.

An unlocked door
is only a problem
if someone tries the handle.

Someone always tries the handle.

– Serguey Asael Shinder

Top comments (0)