đ Original Blog Post : Link
Node.js is extremely popular nowadays, primarily as a backend server for web applications. However, in the world of microservices, you can find it pretty much everywhere, playing different and important roles in a bigger application stack.
One of the advantages of Node.js is the ability to install additional modules, which from the security point of view, provides more opportunities to open back doors. Additionally, the more popular the framework, the more chances that hackers will try to find vulnerabilities. Therefore, you should always take Node.js security seriously. In this post, youâll learn the 10 best practices for securing your Node.js application.
1. Validate user input to limit SQL injections and XSS attacks
Letâs start with one of the most popular attacks, the SQL Injection. As the name suggests, a SQL injection attack happens when a hacker is able to execute SQL statements on your database. This becomes possible when you donât sanitize the input from the frontend. In other words, if your Node.js backend takes the parameter from the user-provided data and uses it directly as a part of the SQL statement. For example:
connection.query('SELECT * FROM orders WHERE id = ' + id, function (error, results, fields) {
if (error) throw error;
// ...
});
The above query is SQL injection vulnerable. Why? Because the id parameter is taken directly from the frontend. Instead of sending just the id, the attacker can manipulate the request and send SQL commands with it. Instead of sending just 4564 (the id of the order), the attacker can send 4564; DROP TABLE ORDERS; and Node.js will wipe your database.
How do you avoid that? There are a few ways, but the basic idea is to not blindly pass parameters from the frontend to the database query. Instead, you need to validate or escape values provided by the user. How to do it exactly depends on the database you use and the way you prefer to do it. Some database libraries for Node.js perform escaping automatically (for example node-mysql and mongoose). But you can also use more generic libraries like Sequelize or knex.
XSS attacks
Cross-Site Scripting (XSS) attacks work similarly to SQL injections. The difference is that instead of sending malicious SQL, the attacker is able to execute JavaScript code. The reason for that is the same as before, not validating input from the user.
app.get('/find_product', (req, res) => {
...
if (products.length === 0) {
return res.send('<p>No products found for "' + req.query.product + '"</p>');
}
...
});
As you can see in the snippet above, whatever the user puts in the search field, if not found in the database, will be sent back to the user in an unchanged form. What that means is that if an attacker puts JavaScript code instead of the product name in your search bar, the same JavaScript code will be executed.
How do you fix that? Again, validate the user input! You can use validatorjs or xss-filters for that.
2. Implement strong authentication
Having a broken, weak, or incomplete authentication mechanism is ranked as the second most common vulnerability. Itâs probably due to the fact that many developers think about authentication as âwe have it, so weâre secure.â In reality, weak or inconsistent authentication is easy to bypass. One solution is to use existing authentication solutions like Okta or OAuth.
If you prefer to stick with native Node.js authentication solutions, you need to remember a few things. When creating passwords, donât use the Node.js built-in crypto library; use Bcrypt or Scrypt. Make sure to limit failed login attempts, and donât tell the user if itâs the username or password that is incorrect. Instead, return a generic âincorrect credentialsâ error. You also need proper session management policies. And be sure to implement 2FA authentication. If done properly, it can increase the security of your application drastically. You can do it with modules like node-2fa or speakeasy.
3. Avoid errors that reveal too much
Next on the list is error handling. There are a few things to consider here. First, donât let the user know the details, i.e., donât return the full error object to the client. It can contain information that you donât want to expose, such as paths, another library in use, or perhaps even secrets. Second, wrap routes with the catch clause and donât let Node.js crash when the error was triggered from a request. This prevents attackers from finding malicious requests that will crash your application and sending them over and over again, making your application crash constantly.
Speaking of flooding your Node.js app with malicious requests, donât directly expose your Node.js app to the Internet. Use some component in front of it, such as a load balancer, a cloud firewall or gateway, or old good nginx. This will allow you to rate limit DoS attacks one step before they hit your Node.js app.
4. Run automatic vulnerability scanning
So far I described a few obvious must-dos. The Node.js ecosystem, however, consists of many different modules and libraries that you can install. Itâs very common to use a lot of them in your projects. This creates a security issue; when using code written by someone else, you canât be 100 percent sure that itâs secure. To help with that, you should run frequent automated vulnerability scans. They help you find dependencies with known vulnerabilities. You can use npm audit for the basic check, but consider using one of the tools described here.
5. Avoid data leaks
Remember what we said before about not trusting the frontend? You not only shouldnât trust what comes from the frontend but also what you are sending to it. Itâs easier to send all data for a particular object to the frontend and only filter what to show there. However, for an attacker, itâs very easy to get the hidden data sent from the backend.
For example, imagine you want to show a list of the users who signed up for an event. You execute a SQL query to get all users for that particular event and send that data to the frontend, and there you filter it to only show the first and last name. But all the data you donât want to show (like usersâ birth dates, phone numbers, email addresses, etc.) is easily accessible via the browser developer console. This leads to data leaks.
How do you solve it? Only send the data thatâs required. If you only need first and last names, retrieve only those from the database. This creates a little bit more work, but itâs definitely worth it.
6. Set up logging and monitoring
You may think that logging and monitoring, while important, arenât really related to security, but that isnât true. Of course, the goal is to make systems secure from the beginning, but in reality, it requires an ongoing process. And for that, you need logging and monitoring. Some hackers may be interested in making your application unavailable, which you can find out without logging. But some hackers will prefer to remain undetected for a longer period of time. For such cases, monitoring logs and metrics will help you spot that something is wrong. With only basic logging, you wonât get enough information to understand if weird-looking requests are coming from your own application, a third-party API, or from a hacker.
7. Use security linters
We talked about automatic vulnerability scanning before, but you can go one step further and catch common security vulnerabilities even while writing the code. How? By using linter plugins like eslint-plugin-security. A security linter will notify you every time you use unsafe code practices (for example using eval or non-literal regex expressions).
8. Avoid secrets in config files
Writing secure code from the beginning will definitely help, but it wonât make your application bulletproof if you end up storing plain text secrets in your config files. This practice is unacceptable even if you store the code in a private repository. Importing secrets from environment variables is the first step, but itâs not a perfect solution either. To be more confident that your secrets arenât easily readable, use secret management solutions like Vault. Whenever using Vault isnât possible, encrypt your secrets when you store them and be sure to rotate them on a regular basis. Many CI/CD solutions allow you to securely store secrets and securely deploy them.
9. Implement HTTP response headers
Many less common attacks can be avoided by adding additional security-related HTTP headers to your application. The most basic mechanisms like CORS will improve the security of your API, but consider using modules like helmet, which will add even more headers in order to secure your application. Helmet can implement eleven different header-based security mechanisms for you with one line of code:
app.use(helmet());
10. Donât run Node.js as root
In the world of Docker and microservices, we often forget about how Node.js is actually executed. Itâs easy to just spin up a Docker container and assume itâs isolated from the host machine so itâs secure. But using Docker doesnât mean that running Node.js as root is not a problem anymore. Combine the ability to run any JavaScript code via an XSS attack with Node.js running as root and youâll end up with unlimited hacking capabilities.
Summary
Securing web applications is important, but tight deadlines sometimes prevent us from properly executing at any given stage. Thatâs why itâs important to consider security at every step of the software development lifecycle, from conception all the way to production.
Thank You for reading till here. Meanwhile you can check out my other blog posts and visit my Github.