Startups often “move fast and break things” to get to market faster and test assumptions. However, they also often do not take security seriously enough. I’ve responsibly disclosed web vulnerabilities in sites such as Pinterest, Amazon, IMDB, and many more smaller startups. Typically, the issues were very simple and avoidable by having a culture of security. Recently, I was sent a link of a site that broke many of the guidelines below. Within three minutes, I had their database credentials. The site had many flaws, but I never even had to mangle query parameters.
The hard rule that catches the vast majority of the vulnerabilities I’ve found is: Don’t trust the client. XSS, SQL injection, etc all follow under this, and are written about elsewhere quite enough. I’ll go a bit beyond these and focus on some specifics in ways you should structure your applications and APIs.
Clearly separate user data and internal data. Clients and users are not always malicious, but you can be exploited by no purposeful wrongdoing. In addition to obviously sanitizing information for display, consider how you surface internal information. It’s rare that you want your database models to match your API models.
Don’t expose internal auto-increment database IDs. When you see URLs like
/post/3244, it’s not hard to guess that
/post/1 may be valid pages as well. Often, it may seem that it’s no big deal to let clients iterate through your data. However, this can often disclose much more information than you want. A local restaurant delivery service exposed order IDs to clients when completing an order. On a different endpoint, they forgot to check permissions when getting order details. Bam, a malicious client sees every order they’ve ever delivered, with addresses, food preferences, payment info, etc. Two easy solutions: store an additional non-incremental ID with your models for client use, or encrypt internal ids before sending to clients.
Give clients as much as they need, and no more. I’ve seen entire user models sent to clients. Recently, one included fields like
primary_email: '$email', primary_email_is_public: false. Tailor your APIs to the current need. Sure, this slows you down when your client engineers realize they needed another field, but it’s far easier to add a field to an API than remove a field.
Errors should give enough information for clients to handle them, but no more. This is a refinement of the previous rule, but is worth repeating. If clients will not handle an error, give nothing additional. The crux of the vulnerability that gave database credentials was that 500 error screens dumped environment and configuration variables (which may be helpful during debug).
APIs are for computers to consume, and for humans to implement. Usually, let the client concern itself with presentation of typical error messages. I’ve seen error page URLs with
?error=Sorry%2C%20your%20session%20is%20not%20valid. A good portion of the time, since the message is coming from out-of-band from their normal templating, there’s a XSS here. There’s also a practical benefit for using error codes vs messages: the language of the error will not break any special casing on the client. At Kifi, we once ran into an issue where there as a notification “You have a new follow to your Library $name”. Upon fixing the capitalization of Library→library, we realized some clients used string matching on the message to modify the display, and the display was breaking.
Treat all your APIs with the same level of carefulness. Mobile APIs are notoriously weak, because engineers assume that no one will see them. A social network built on the concept of privacy and anonymity had a vulnerability that allowed any user to identify who another user was. The exact issue was pretty simple if you had seen it in your browser’s dev tools, but since it required a MITM proxy to identify, had gone unnoticed.
Not all engineers in your company will be security-minded, so encourage a culture of security where you follow best-practices as a habit. Perfect security doesn’t exist, but big problems can be systemically avoided. Pretend that your API traffic is public knowledge, and design your systems defensively.