--- title: >- How to Secure a Webapp description: >- Get ready to jump through some hoops. tags: tech --- In this post I will be documenting all security hoops that one must jump through in order to consider their webapp secure. This list should not be considered comprehensive, as I might have forgotten something or some new hoop might have appeared since writing. For the context of this post a "webapp" will be considered to be an HTML/CSS/JS website, loaded in a browser, with which users create and access accounts using some set of credentials (probably username and password). In other words, most popular websites today. This post will only cover those concerns which apply to _all_ webapps of this nature, and so won't dive into any which might be incurred by using one particular technology or another. Some of these hoops might seem redundant or optional. That may be the case. But if you are building a website and are beholden to passing some third-party security audit for any reason you'll likely find yourself being forced to implement most, if not all, of these measures anyway. So without further ado, let's get started! ## HTTPS At this point you have to use HTTPS, there's not excuse for not doing so. All attempts to hit an HTTP endpoint should redirect to the equivalent HTTPS endpoint, and you should be using [HSTS][hsts] to ensure that a browser is never tricked into falling back to HTTP via some compromised DNS server. [hsts]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security ## Cookies Cookies are an old web technology, and have always been essentially broken. Each cookie can have certain flags set on it which change their behavior, and some of these flags are required at this point. ### Secure If you're storing anything sensitive in a cookie (spoiler alert: you will be) then you need to have the Secure flag set on it. This prevents the cookie from being sent in a non-HTTPS request. ### HTTPOnly The HTTPOnly flag protects a cookie from XSS attacks by preventing it from being accessible from javascript. Any cookie which is storing sensitive information _must_ have this flag set. In the **Authentication** section we will cover the storage of session tokens, but the TLDR is that they have to be stored in an HTTPOnly cookie. Practically, this means that your sessions architecture _must_ account for the fact that the webapp itself will not have direct access to its persistent session token(s), and therefore must have some other way of knowing that it's logged in (e.g. a secondary, non-HTTPOnly cookie which contains no secrets but only signals that the browser is logged in). ### SameSite The SameSite attribute can be set to `Strict`, `Lax`, or `None`. `Lax` is the default in modern browsers and is sufficient for most security concerns, but if you can go with `Strict` that would be better. The downside of `Strict` is that cookies won't be sent on initial page-load of a site. In any case, even though `Lax` is the default you should still set this attribute manually (or your auditor might get to add another bullet point to their report). ## Authentication Authentication is obviously one of the juiciest targets for an attacker. It's one thing to be able to trick a user into performing this or that action, but if one can just log in _as_ the user then they essentially have free-reign over all their information. ### Password History Most websites use a username/password system as the first step of login. This is.... fine. We've accepted it, at any rate. But there's a couple of hoops which must be jumped through as a result of it, and the first is password history. I hope it goes without saying that one should be using a hashing algorithm like bcrypt to store user passwords. But what is often not said is that, for each user, you need to store the hashes of their last N passwords (where N is something like 8). This way if they attempt to re-use an old password they are not able to do so. The users must be protected from themselves, afterall. ### Credential Stuffing/Account Enumeration A credential stuffing attack is one where credentials are stolen from one website and then attempted to be used on another, in the hope that users have re-used their username/password across multiple sites. When they occur it'll often look like a botnet spamming the authentication endpoint with tons of different credentials. Account enumeration is a similar attack: it's where an attacker finds a way to get the webapp to tell them whether or not an account email/username exists in the system, without needing to have the right password. This is often done by analyzing the error messages returned from login or a similar endpoint (e.g. "Sorry this username is taken"). They then run through all possible values for that endpoint to try and enumerate which users actually exist in the system. Account enumeration is tricky because often those errors are extremely helpful, and we'd _like_ to keep them if we can. I've bucketed both of these attacks in the same section because they have a similar solution: proof-of-work. The idea is that, for each request to some sensitive endpoint, the client must send some proof that they've done an intensive CPU computation. Compared to IP-based rate-limiting, PoW is much more effective against botnets (which have a limitless set of IPs from which to spam you), while also being much less intrusive on your real users than a captcha. PoW stymies botnets because they are generally being hosted by low-power, compromised machines. In addition the systems that run these botnets are pretty shallow in capability, because it's more lucrative to rent the botnet out then to actually use it yourself, so it's rare for a botnet operator to go to the trouble of implementing your PoW algorithm in the first place. So stick a PoW requirement on any login or account creation endpoint, or any other endpoint which might be used to enumerate accounts in the system. You can even make the PoW difficulty rise in relation to number of recent attempts on these endpoints, if you're feeling spry. ### MFA All the PoW checks in the world won't help your poor user who isn't using a different username/password for each website, and who got unlucky enough to have those credentials leaked in a hack of a completely separate site than your own. They also won't help your user if they _are_ using different username/passwords for everything, but their machine gets straight up stolen IRL and the attacker gets access to their credential storage. What _will_ help them in these cases, however, is if your site supports multi-factor authentication, such as [TOTP][totp]. If it does then your user will have a further line of defense in the form of another password which changes every 30 seconds, and which can only be accessed from a secondary device (like their phone). If your site claims to care about the security of your user's account then MFA is an absolute requirement. It should be noted, however, that not all MFA is created equal. A TOTP system is great, but a one-time code being sent over SMS or email is totally different and not nearly as great. SMS is vulnerable to [SIM jacking][sim], which can be easily used in a targeted attack against one of your users. One-time codes over email are pointless for MFA, as most people have their email logged in on their machine all the time, so if someone steals your user's machine they're still screwed. In summary: MFA is essentially required, _especially_ if the user's account is linked to anything valuable, and must be done with real MFA systems like TOTP, not SMS or email. [totp]: https://www.twilio.com/docs/glossary/totp [sim]: https://www.vice.com/en/article/3kx4ej/sim-jacking-mobile-phone-fraud ### Login Notifications Whenever a user successfully logs into their account you should send them email (or some other notification) letting them know it happened. This way if it wasn't actually them who did so, but an attacker, they can perhaps act quickly to lock down their account and prevent any further harm. The login notification email should have some kind of link in it which can be used to immediately lock the account. ### Token Storage Once your user has logged into your webapp, it's up to you, the developer, to store their session token(s) somewhere. The question is... where? Well this one's easy, because there's only one right answer: HTTPOnly cookies (as alluded to earlier). When storing session tokens you want to guard against XSS attacks which might grab the tokens and send them to an attacker, allowing that attacker to hijack the session and pose as the user. This means the following are not suitable places to store the tokens: * Local storage. * `window`, or anything which can be accessed via `window`. * Non-HTTPOnly cookies. Any of these are trivial to find by a script running in the browser. If a session token is ephemeral then it may be stored in a "normal" javascript variable somewhere _as long as_ that variable isn't accessible from a global context. But for any tokens which need to be persisted across browser restarts an HTTPOnly cookie is your only option. ## Cross-Site Speaking of XSS attacks, we have some more mitigation coming up... ### CSP Setting a [CSP][csp] for your website is key to preventing XSS. A CSP allows you to more tightly control the allowed origins of the various entities on your site (be they scripts, styles, images, etc...). If an entity of unexpected origin shows up it is disallowed. Be sure to avoid any usages of the policies labeled "unsafe" (go figure), otherwise the CSP is rendered somewhat pointless. Also, when using hostname based allowlisting try to be as narrow as you can in your allowlist, and especially only include https hosts. If you can you should opt for the `nonce` or `sha` policies. [csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP ### SVG A small but important note: if you're website allows users to upload images, then be _very_ careful about allowing users to upload SVGs. SVGs are actually XML documents, and even worse than that they allow `