Node.js is one of the favorite technologies for developers when it comes to backend development. Its popularity keeps rising and is now one of the main targets of online attacks. That is why it is crucial to secure Node.js from vulnerabilities and threats.
In this guide, you will see the 15 best practices for devising a secure Node.js app architecture for production. Implement them all to make your backend more secure than ever!
Building a secure Node.js application is important for at least three good reasons:
You might think that security problems are not that widespread and it is enough to follow coding and architectural best practices, but this is not true. The power of Node.js lies in the NPM environment, which offers millions of libraries. The problem is that most NPM packages involve some security vulnerabilities.
In other words, your Node.js project is not safe if its dependencies are not. Considering how popular it is to use external libraries, this is worrisome. According to the Snyk State of Open Source Security report, an average Node.js project has 49 vulnerabilities out of 79 direct dependencies. That concerning statistic underscores how important it is to protect your Node.js application.
Let’s take a look at the most popular best practices to secure your Node.js project.
Running Node.js with root privileges is not recommended as it goes against the principle of least privilege. No matter if your backend is on a dedicated server or Docker container, you should always launch it as a non-root user,
If you instead run Node.js with root privileges, any vulnerabilities in your project or its dependencies can potentially be exploited to gain unauthorized access to your system. For example, an attacker could harness them to execute arbitrary code, access sensitive files, or even take control of the entire machine. Thus, using root users for Node.js must be avoided.
The best practice here is to create a dedicated user for running Node.js. This user should have only the permissions required to launch the app. This way, attackers who succeed in compromising your backend will be restricted to that user’s privileges, limiting the potential damage they can cause.
NPM libraries make it easier and quicker to build a full-featured Node.js backend. At the same time, they can also introduce security risks into your application. New vulnerabilities are discovered all the time, and it is the maintainers’ job to address them and release an updated version of the package. Here is why you should keep your dependencies up to date.
To ensure the NPM libraries you are relying on are secure, you can use npm audit
and snyk
. These tools analyze your project’s dependencies tree and provide insights into any known vulnerabilities.
Here’s an example of running npm audit
:
npm audit
...
found 4 vulnerabilities (2 low, 2 moderate)
run `npm audit fix` to fix them, or `npm audit` for details
This command leverages the GitHub Advisory Database and checks your package.json
and package-lock.json
against known security issues.
Additionally, you can use Snyk to check your dependencies against Snyk’s Open Source Vulnerability Database. Install snyk
with:
npm install -g snyk
In the root folder of your project, test your application with:
snyk test
To open a wizard that will walk you through the process of patching the vulnerabilities found, run:
snyk wizard
Using snyk
and regularly running npm audit
helps you identify and fix security issues in your NPM libraries before they might become a problem. Remember that the security of your app is only as strong as the weakest link in your dependencies.
The cookie names used by your Node.js application can unintentionally reveal the technology stack your backend is based on. That is valuable information that you should always obscure, as attackers can use it again you. By knowing what framework you are using, they can exploit specific weaknesses associated with it.
In detail, attackers tend to focus on the name of the session cookie. Protect your app from that by setting a custom session cookie name with the express-session
middleware:
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
// set a custom name for the session cookie
name: 'myCustomCookieName',
// a secure secret key for session encryption
secret: 'mySecretKey',
}));
The default HTTP headers in Express are not very secure. We check header security with the online Security Headers project:
Some of these headers contain information that should not be publicly exposed, such as X-Powered-By
. Others are missing and should be added to deal with various security-related aspects, including preventing cross-site scripting (XSS) attacks.
Here is where helmet
comes into play! This library takes care of setting the most important security headers based on the recommendations from Security Headers. Use it as follows:
const express = require('express');
const helmet = require('helmet');
const app = express();
// register the helmet middleware
// to set the security headers
app.use(helmet());
The helmet()
middleware automatically removes unsafe headers and adds new ones, including X-XSS-Protection
, X-Content-Type-Options
, Strict-Transport-Security
, and X-Frame-Options
. These enforce best practices and help protect your application from common attacks.
The headers set by your Node.js app will now be considered secure:
DDoS (Distributed Denial of Service) and brute force are two of the most common web attacks. To mitigate them, you can implement rate limiting. This technique involves controlling the incoming traffic to your Node.js backend, preventing malicious actors from overwhelming your server with too many requests.
The easiest way to implement rate limiting is through the rate-limiter-flexible
library. This dependency provides a configurable middleware to restrict the number of requests coming from the same IP address or user within a specified time frame.
Here is an example of how to use it to apply rate limiting in Node.js:
const express = require('express');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const app = express();
const rateLimiter = new RateLimiterMemory({
points: 10, // maximum number of requests allowed
duration: 1, // time frame in seconds
});
const rateLimiterMiddleware = (req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => {
// request allowed,
// proceed with handling the request
next();
})
.catch(() => {
// request limit exceeded,
// respond with an appropriate error message
res.status(429).send('Too Many Requests');
});
};
app.use(rateLimiterMiddleware);
First, a rate limiter instance allowing a maximum of 10 requests in 1 second is initialized. Then, it is used in a custom middleware to analyze the IP of the incoming request. If the rate limit is not exceeded, the request proceeds. Otherwise, the request gets blocked and the server returns a 429 Too Many Requests
response.
To protect your Node.js application against attacks that exploit user authentication, you need to enforce strong authentication policies. First, invite users to set strong passwords. Also, you should support Multi-Factor Authentication (MFA) and Single Sign-On(SSO). MFA adds an extra layer of security by requiring users to provide multiple forms of authentication, while SSO simplifies the authentication process and reduces the risk of weak or reused passwords.
When it comes to hashing passwords for storage, prefer strong cryptographic functions like bcrypt
over the methods offered by the Node.js crypto library. That package provides a secure password-hashing algorithm that makes it significantly harder for attackers to crack passwords. Finally, mitigate brute-force attacks by restricting the number of failed login attempts through rate limiting as explained earlier.
Any information provided to the attacker unintentionally can be used against you. For this reason, server responses should contain only what the caller strictly needs. For example, avoid returning detailed error messages or stack traces directly to the client. Instead, provide generic error messages that do not reveal specific implementation details. The easiest at to do so is to run Node.js in production mode setting the NODE_ENV=production
env, otherwise Express will add the stack trace in the error responses.
Similarly, you must be careful about the data included in API responses. Return only necessary data fields and avoid exposing sensitive information not requested by the caller. This will minimize the risk of accidentally disclosing confidential or privileged information.
Your backend in production may be under attack, and you may not even be aware of it. Here is why it is essential to monitor your Node.js application. By connecting it to an Application Performance Monitoring (APM) tool, you can keep track of it to identify security issues and ensure its overall health.
Fortunately, there are several APM libraries and services available for Node.js. Some of the most popular are SigNoz, Sentry, Prometheus, New Relic, and Elastic These provide information on various aspects of the application, including performance, error rates, resource usage, and security-related metrics. In particular, they enable real-time data collection and detection of anomalies or suspicious activity that could indicate a security breach. Some of them also offer osservability features to also track the deployment workflow in your CI/CD pipeline.
By ensuring that your backend is accessible only via HTTPS, you will improve the confidentiality of data exchanged between clients and your Node.js server. HTTPS establishes an encrypted channel that safeguards sensitive information like passwords, session tokens, and user data from interception.
As part of such policy, you should also use HTTPS cookies. To achieve that, make sure that any cookies set by your Node.js application are marked as secure
and httpOnly
`:
res.cookie('myCookie', 'cookieValue', {
// create an HTTPS cookie
secure: true,
httpOnly: true,
});
Unintended parties or scripts will no longer be able to access your cookies. Plus, they will be transmitted exclusively over HTTPS connections.
Whenever users have the opportunity to enter inputs, attackers can exploit that to send malicious data to the server. Therefore, validating user input is fundamental for ensuring the security and integrity of a Node.js application.
Thanks to libraries like express-validator
, you can enforce strict validation rules on the body and query parameters of incoming requests. Only data in the expected format will be allowed to pass through, avoiding unexpected behavior due to unforeseen input.
Security linters analyze your codebase to identify vulnerabilities, unsafe code sections, and best practice violations. One of the most popular is eslint-plugin-security
, a set of ESLint rules to enforce security development in Node.js.
By integrating such tools into your development workflow, you can spot and address security issues early on. Specifically, they reduce the risk of introducing vulnerabilities into your application while coding. These tools particularly effective when added integrated in the CI/CD pipeline.
SQL injection is a common security vulnerability that occurs when an attacker can manipulate input data passed into an SQL query. This generally happens when concatenating user input directly into SQL queries. In that scenario, attackers can forge specific inputs aimed at executing arbitrary SQL code, leading to unauthorized access and data breaches.
There are several ways to prevent SQL injection in Node.js. The most popular ones are:
The default request body size limit in Node.js is 5 MB. To protect your backend from DDoS attacks where malicious users try to flood your server with data, it is recommended to reduce that limit. To do so, you can use the body-parser
library and configure it as below:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// set the request size limit to 1 MB
app.use(bodyParser.json({ limit: '1mb' }));
Adjust the value according to your specific requirements. Now, requests with a body larger than 1 MB will now be blocked immediately, preventing the server from allocating resources to process them.
Automated vulnerability scanning tools, such as SonarQube or similar, are valuable resources for identifying security problems in Node.js applications. These tools perform comprehensive scans of the codebase, dependencies, configurations, and other components to identify security weaknesses.
Here are some key benefits of using automated vulnerability scanners:
Giving users and security researchers the ability to report vulnerabilities found in your Node.js backend is another important aspect to keep your app secure. Not only should this be possible, but the procedure needs to be clear and accessible.
An effective approach to allow researchers to reach out to you is the security.txt
proposed standard. This is a simple text file placed at the root of your project that provides information on how to report security vulnerabilities. It follows a standardized format and includes contact details, encryption methods, and disclosure guidelines.
Here’s an example of a basic security.txt
file:
Contact: [email protected]
Encryption: https://example.com/pgp-key.asc
Contact
specifies the email address security vulnerabilities or concerns should be reported to. These emails may contain critical information and should not be publicly accessible. So, the Encryption
field indicates the location of the organization’s PGP public key that can be used for encrypting messages inside the e-mails. This mechanism ensures that only the organization can decrypt those messages with the private key and read them.
Similarly, you could also consider adding a “Report Vulnerability” page on your website.
In this guide, we discussed ways of securing Node.js applications in production. As developers, it is our duty to protect user data, maintain application functionality, and preserving our reputations.
By integrating these techniques, approaches, and tips, you can create a more reliable Node.js architecture, mitigating risks and ensuring the security of your application.
Source: Semaphore