Web security is a huge topic. The things you need to be aware of in order to keep your users (and yourself) safe can be overwhelming. Nevertheless, there are a few concepts and recommendations that solve the most important problems and are easy to learn and understand. Let’s take a look.
According to Firefox, around 80% of page loads use HTTPs, so this is already a well-established practice. Using HTTPs allows your users to have security and privacy. It’ll encrypt the information between them and your servers, including passwords and other sensitive information such as email, physical addresses, etc. Years ago setting up HTTPs in your servers could be hard and expensive but now thanks to Let’s Encrypt it is a lot easier and free.
Browsers have the ability to enable some security mechanisms but only if you tell them to through HTTP headers. For example, you can tell the browser to forbid loading your website inside an iframe using the X-Frame-Options
header. This will prevent your users from being the target of clickjacking attacks.
Many of these headers and what they do can be found on the helmetjs website. Helmet.js is an excellent and easy-to-use library that allows you to enable these security mechanisms easily to express applications.
Most of these headers are easy to use and then we have the Content-Security-Policy
header, which is a bit more complicated and is not enabled by default by Helmet.js. With this header, you can configure which URLs are allowed or disallowed to load JavaScript, CSS, images, etc. The idea of this header is to mitigate any code injection attacks. For example, if an attacker figures out how to inject JavaScript in your website, they will probably want to gather some information from your users and send that through AJAX to a server under their control. However if you have a Content Security Policy header (CSP) properly set up, even if they can inject JavaScript they won’t be able to send the stolen information to their servers.
If you use identifiers in URLs provided by users (e.g. http://example.com/my-super-secret-project-name) then when implementing authorization you shouldn’t return 403 if the resource exists but the user doesn’t have access to it. If you do this you are implicitly telling an attacker that the resource exists. In this case, you should return a 404 so the attacker doesn’t know whether the resource exists and they just don’t have access to it or it doesn’t exist at all.
Authentication is the most complex topic in web security in my opinion. You could write entire books about it and still not be able to cover everything. Nevertheless, there are a few aspects that are basic and not too hard to implement.
Of course, don’t store passwords in plain text. You need to store something in a secure way to be able to validate a user’s password when they log in. You don’t necessarily need to store the password itself but something that allows you to match against what the user is introducing in the login form. There’s a nice cryptographic primitive called hash that allows you to just do that.
A hashing function receives the plain text and outputs a value that you would normally store as hexadecimal characters. The thing is that calculating the plain text from the output is pretty hard. So even if somebody is able to steal your database they will have a hard time calculating the passwords from the hashes. But, how do you use a hashing function? It’s easy! When the user enters the password for the first time (or wants to change it) you store hash(users_input)
and when they log in you compare the stored value with the password they are providing hash(password) == stored_value
. But, you better use a timing safe comparison such as crypto.timingSafeEqual to avoid timing attacks.
This is a good start but there are a couple of additional things to keep in mind. First, you should salt the password, because plain hash functions will return the same output for the same input. This means that you could generate a list of hashes with the most common passwords like this, hash('1234')
, hash('love')
and compare it to what it’s stored in the database. If you are an attacker with a dump of the database this won’t give you everybody’s passwords but it’ll give you a lot of them!
This is called a rainbow table. In order to prevent this, you can generate a random number (called salt) that you can store in plain text near the password hash and then calculate the hashes with hash(salt + password)
.
What else? Well, you should also either choose a slow hashing function or hash multiple times, because some hash functions are very fast, which is a security concern. Why? Because if an attacker is really interested in somebody’s password/access they might try with thousands or millions of passwords to see if they can crack the hash. In that case, you are making their work a lot simpler if the hash function is fast. However, if the hash function is slow (e.g. 300ms vs 10ms) you are making things way slower for them. Imagine 30x slower may mean taking 30 years instead of 1 year to crack it.
What hashing function should I use? Ok, good question. Basic functions such as SHA and MD5 are not good for hashing passwords. For hashing passwords, you’d prefer to use bcrypt, scrypt, or pbkdf2.
Users tend to forget their passwords so you need a mechanism to allow them to identify themselves, somehow, and be able to set a new password. This can be tricky because you may be introducing a security breach depending on how you do it. Usually, you’d send an email to their email address providing a reset link. This link should expire and should have some randomness so an attacker cannot build reset links. Those are the two basic things to have in mind. Nevertheless, there are many other things that you may want to have into account and for that, I recommend this guide.
If you enter invalid credentials in the login screen of your operating system you may realize that it takes a bit to make the login form enabled again. Why is that? Simple, for the same reason we wanted our hash functions to be slow we want to mitigate brute force attacks by doing things a bit slower. So slow that for the user it’s not a big deal, but for the attacker, it is a big pain point.
Nevertheless, this would only stop a simple brute force attack that doesn’t do requests in parallel. For additional protection, you should rate-limit login attempts by IP, and if you want to go extra secure, in order to avoid brute force attacks to the same user from different IPs you should rate-limit by user account.
Complicated, huh? Yeah, like I said, you could write an entire book just dedicated to this topic. However, everything depends on how valuable the information you have is.
As you can see passwords can be problematic. Even if you do all the things right (like properly calculating and storing hashes, doing the reset functionality as secure as possible, etc) you simply cannot stop people from doing things like reusing the same password in many services or choosing a weak password that people close to them may guess. Are there any alternatives? Well, there are, here are a few:
This is one of the most common security vulnerabilities out there and it’s not that hard to fix. Let’s see how it works. The most common way of implementing session management consists of using cookies. Once a user is authenticated you set a cookie which is received by the browser and it sends it automatically in every request to the server. This is great and simple. However, let’s think about this. An attacker crafts a website with a hidden <form action=”vulnerablewebsite/malicious_action”>. Imagine it’s the website of a website to transfer goods or money and the attacker crafts a URL that, when submitted, will make the logged user transfer something to the attacker.
Now the attacker just needs to send a malicious link that contains the <form> to the victim. Once the victim visits the link, the form can be submitted even silently, and the request is automatically authenticated because the cookie with the auth information is sent by the browser. The attacker doesn’t need to even know the content of the cookie. And the malicious link can be a website hosted anywhere because browsers, by default, do not prevent forms from having URLs pointing to other domains.
How can we avoid this? The solution is to generate a token and put this token in a new cookie and in a hidden field in the form. Then when the form is submitted the backend will check if the token from the cookie is equal to the token in the form. An attacker doesn’t see the cookies so it’s unable to craft a form with a valid CSRF token.
If you are using express you can use the csurf package that will generate tokens, put them in cookies and validate them for you.
This is maybe the most dangerous security vulnerability you may have and consists of modifying input parameters to manipulate poorly written queries in the application code. For example, if in your code you have:
query = "SELECT * FROM users WHERE login = '" + input_login + "';"
An attacker could send a malicious input_login
parameter in order to change the intent of the SQL query, even including multiple sentences separated by ;
. With this mechanism, an attacker could bypass the user authentication or even delete records in your database.
The main mechanism to void this problem is by escaping the input parameters. Any good SQL library should have a way to achieve this. For example, the pg library allows you to do this:
const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *' const values = ['brianc', '[email protected]'] const response = await client.query(text, values)
Instead of interpolating the values manually, you use placeholders ($1, $2), that the library will replace by the escaped version of the array of values.
To make sure you never forget to use placeholders you could set up a linter that catches manual interpolations and gives you an error.
This is the third biggest security vulnerability. It happens when a web application does not sanitize and/or escape the output of a value. For example, if your application allows users to send messages to each other and you don’t escape the messages when rendering your site, if a user inserts HTML on them, the HTML will be rendered and evaluated directly by the browser allowing an attacker to inject JavaScript on it.
So you need to escape the output. For example in EJS you’d do:
<div><%= message %></div>
If message
contains <script>…</script>
, the template engine will escape it to <script>…</script>
and the browser won’t evaluate the script content.
If you want to allow users to use some HTML in their content, but you want to avoid XSS attacks then you should clean up the HTML first allowing just some tags and attributes to be used. For JavaScript, you have this sanitizer.
There’s a super simple attack that is also super simple to avoid and that’s why I wanted to mention it here. If you have a website that contains links to external websites because you put them there or because users can leave links in messages or their profile or anywhere, you are probably using <a target="_blank">
to make those links open in a new window or tab. That’s nice, but it’s a potential security problem because the target website has access to the original tab by using window.opener
. The solution is as easy as using these values for the rel
attribute:
<a href=”...” target=”_blank” rel=”noopener noreferrer”>Malicious link</a>
You should do this for any link with target="_blank"
even if you know that the website you are linking is not malicious, because it could have been attacked and it could contain malicious JavaScript code.
Nowadays there are also tools that allow you to catch problems, including security problems, easily. One of them is webhint. It has some rules that catch problems such as poor HTTP headers, vulnerable external links, etc.
There are also more advanced tools such as OWASP ZAP if you are interested in digging deeper into these topics.
Like I said, web security can be overwhelming, but I hope this article lets you understand the most common attacks and how to avoid or mitigate them. Let’s recap the most important things:
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Web security 101"
Your simplicity of presentation is motivating for learning. Please keep it up for you teeming followers