DEV Community 👩‍💻👨‍💻

C. Dylan Shearer
C. Dylan Shearer

Posted on

RESTful Security: Plug the Leaks!

TL;DR: Don't leak information through HTTP error codes.

Here is a possible vulnerability that is probably not very serious but easy to overlook. Suppose we get hired to redo a local bank's website. Being the hip coders that we are, we decide to make a nice RESTful API.

One of our API's operations is the following:

GET /api/accounts/{acctId}
Enter fullscreen mode Exit fullscreen mode

Of course, this operation succeeds only if an account with ID acctID exists and the request contains valid credentials for the customer that owns that account. If one of these conditions does not hold, we return an appropriate HTTP error code.

What should that error code be? We know that RESTful APIs return the most appropriate status code for the situation. So, it seems to make sense to return 404 when the account doesn't exist, and 401 when it does but the user is unauthenticated or isn't its owner.

But this may well be a bad idea, because an unauthenticated user can now do something like this:

$ for i in `seq 300 305`; do curl -sI https://exammple.com/api/accounts/${i} | head -n 1; done
HTTP/1.1 404 Not Found
HTTP/1.1 401 Unauthorized
HTTP/1.1 401 Unauthorized
HTTP/1.1 404 Not Found
HTTP/1.1 401 Unauthorized
HTTP/1.1 404 Not Found
Enter fullscreen mode Exit fullscreen mode

This unauthenticated user has now learned something: that there are no accounts with IDs 300, 303, and 305, but there are with IDs 301, 302, and 304. And of course running this command with a wider range of IDs would reveal even more information.

How bad is this? That depends on a lot of other things; at this point, the most we can say is that it is not obviously terrible. But still, this could be used to learn other things --- for example, the rate at which new accounts are created, which might be interesting to a competitor.

The important point is that this API leaks information to users that have no legitimate reason to have it. And it is easy to fix: we should just return 404 even if the account does in fact exist.

Conclusion

While we focus on the obvious security concerns --- keeping account balances secret, preventing unauthorized transactions --- it is easy to overlook how the behavior of our system leaks sensitive information.

An attacker's goal is to gain information about the state of our system by observing its behavior. So our job is to make sure that the system behaves (from the attacker's point of view) identically regardless of its state. And remember: unauthenticated users are not the only potential attackers. Even logged-in customers should get 404 in response to requests for accounts that they don't own.

Top comments (11)

Collapse
sokolovdenis profile image
Denis Sokolov

Maybe "more RESTFully" logic may be:
return 401 if user unauthenticated (it's does not matter exists account or not);
return 403 if user authenticated but is not owner of requested account (it's does not matter exists account or not);
return 404... I think never :)

Collapse
sqlrob profile image
Robert Myers

That has the exact same problem as 401 / 404, it just requires the the additional step of registering. There is little to no security gain.

Collapse
sokolovdenis profile image
Denis Sokolov

If there is no authentication in API, and we want to protect our growth rate statistics - yes, same problem exists. And GUID instead of INT identifiers may help.

Thread Thread
sqlrob profile image
Robert Myers

Even if there is authentication in API, it doesn't matter. (See Panera's "fix")

GUID or other unpredictable identifier is the only real fix. Rate limiting each user can help as well, but how useful that is really depends on how hard it is to get authentication tokens.

Collapse
dshearer profile image
C. Dylan Shearer Author

That certainly makes sense.

Collapse
rhymes profile image
rhymes

Agreed, also don't use internal db ids in the api so the attacker cannot infer the sequencing

Collapse
neilmadden profile image
Neil Madden

If your external ids are unguessable (e.g., 256-bit random strings) then this attack completely disappears. Another alternative is to only expose /api/accounts/me if there is no valid reason for a user to ever access any other account.

Collapse
danidee10 profile image
Osaetin Daniel

Yep.It's always good to use a random string or a different Identifier for any public resource.

Collapse
jonerer profile image
Jon Mårdsjö • Edited on

Not to say this is not important, but there is more to it if you want to plug the informational leaks in a serious way.

Using the same example as in the OP, let's say example.com/api/accounts/300 and 301 both return the same HTTP code. Another thing to think about then is timing -- let's say an attacker can do a thousand requests for each of the accounts.

If they find that on average accounts 301, 302 and 304 take 10ms, but 300, 303 and 305 take 8.5ms -- then they have found that there's a difference. Perhaps the backend code is written so that if you find that the account exists, you do a credentials check; but if the account doesn't exist you skip the credentials check. Then an attacker can still know whether the account exists or not, given the timings.

Collapse
jonerer profile image
Jon Mårdsjö

Btw this is also the reason why checking an auth token with == is not a valid practice -- you have to use a "time-secure" comparison

Collapse
mattcanello profile image
Mateus Canello Ottoni

I wrote an article about building better web APIs and I pointed out that I use a similar approach: return 404 not only when the resource does not exist, but also when it does exist and the authenticated user doesn't own it.

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.