Node.JS Security Best Practices

Node.js is an open source development platform for executing JavaScript code server-side. Learn more about Node.JS security best practices.

January 5, 2023

What is Node.JS?

Node.js (Node) is an open source development platform for executing JavaScript code server-side. Node is useful for developing applications that require a persistent connection from the browser to the server and is often used for real-time applications such as chat, news feeds and web push notifications.

Node.js is intended to run on a dedicated HTTP server and to employ a single thread with one process at a time. Node.js applications are event-based and run asynchronously. Code built on the Node platform does not follow the traditional model of receive, process, send, wait, receive. Instead, Node processes incoming requests in a constant event stack and sends small requests one after the other without waiting for responses.

This is a shift away from mainstream models that run larger, more complex processes and run several threads concurrently, with each thread waiting for its appropriate response before moving on.

In this article:

Most Common Node.js Security Attacks and Vulnerabilities

Code Injection

Writing a secure code for an application is a developer’s primary responsibility. However, when using open-source packages, you can’t entirely guarantee your codebase’s security. Code injection refers to any attack where the attacker injects the code into the system and forces the application routine to execute it. The attacker explores the poorly handled and untrusted data to gain insights into your codebase.

A  common reason for this security risk is improper input and output data validation. SQL injection is a recurring code injection attack most people encounter during software development. Here, the attacker uses the malicious SQL code to manipulate the backend database and gain access to sensitive information which is not generally visible.

Cookies help websites or web applications to identify a particular user since any user action on the web application gets stored as a cookie in the underlying infrastructure. Shopping carts in eCommerce websites are the most common examples of cookies. The cookies will remember the items you select on the website, and when you move to the checkout page, the shopping cart will display those items.

However, the problem with Node.js development arises when the developer opts for the default cookie names instead of customizing them as per the need. Since attackers are aware of the default cookie name, they are likely to attack and access the user input under the rich ecosystem effortlessly based on this information.

Brute-Force Attacks

Brute force attacks are among the most recurrent attacks or risks you will find in any Node.js security checklist. The attackers generate random passwords and try implementing them on login endpoints of web applications to access critical information. Brute forcing is all about making millions of combinations until you find the correct password for the web application. To prevent brute-force attacks, you will need to strengthen your authentication mechanism for Node.js applications. Additionally, you can also limit the number of login attempts from one IP to deal with such risky situations and utilize bcrypt.js to safeguard the passwords stored in the database.

Cross-Site Scripting (XSS) Attack

Cross-site scripting attacks are vital threats you need to deal with while working on Node.js web application development. Cross-Site Scripting (XSS) enables attackers to inject client-side scripting involving tweaked JavaScript code to the web app caused by missing input validation of hostnames returned by Domain Name Servers.

An attacker can use XSS to send a malicious script to the end-user, and the end-users browsers have no way to identify the trustworthiness of the codebase. As a result, they execute it by default, and attackers can access any cookies, session tokens, or other sensitive information. These scripts can also rewrite the content of any HTML page, making  XSS quite fatal.

Cross-Site Resource Forgery (CSRF)

CSRF is a form of session hijacking where a user is forced to run malicious actions on an application that they’re currently authenticated to. In a CSRF attack, attackers hijack the sessions of real users, thereby bypassing security rules for non-users. 

The primary aim of CSRF attackers is to change the state of the application by using social engineering techniques like sending a message or an email to the users. CSRF attacks can cause significant damage to Node.js apps as it forces users to change their email addresses and transfer funds. For admin-level users, CSRF attacks can also compromise the entire web application security and must be mitigated.

Exploited and Malicious Packages

There were multiple instances of supply chain compromises involving packages hosted on Node Package Manager (NPM), the package manager for the Node.js JavaScript platform, either being compromised directly to deliver malware or simply being created to impersonate popular, legitimate packages.

By compromising a popular package used by developers, it is easy to amplify the distribution of malicious code directly to victims themselves at scale. This can be done either through dependency confusion, hijacking weak credentials, exploiting vulnerabilities to access the target code or using the names of packages abandoned by their developers.

Node.js Security Best Practices

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.

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.

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.

Using Security Linters that Capture Vulnerabilities in Code

Code linters help developers identify various issues in the code before compiling. They can detect the most common issues and force developers to follow best practices.

These code linters come with their own rules that the developers can customize depending on the requirement. So, developers must enable the rules related to security vulnerability detection from the linter configuration before using them.

Server-side Logging and Monitoring

Using a good logging library would give the developer more robust features to troubleshoot and monitor activities. On the other hand, logging unwanted or too many logs can impact the application’s performance and utilize more resources. Therefore, we should only use a reasonable logging level when deploying the application in a production environment.

When structuring the log messages, it is essential to format them to make it easy for humans and machines to read and understand the logs. Logging vague messages will lead to misunderstandings among the developers.

The following example shows how we can write a descriptive log message.

Incorrect Description: Service Failed, Not Working

Correct Description: Application worker service failed due to insufficient disk space, ensure that adequate disk space is available and restart the Application worker service.

Further, it is important to ensure that the logging mechanism captures all relevant information, including the IP address, username, actions performed, etc.

Capturing or storing sensitive information within the application logs is not recommended. It is a violation of major application compliance requirements such as PCI, GDPR, etc. However, there can be scenarios where we need to log such information. In such cases, it is recommended to mask the sensitive information before it is collected and written into the logs.