DEV Community

Ishikha Rao
Ishikha Rao

Posted on

NodeJS Security Best Practices

Introduction

Node.js is a popular processing system for JavaScript that is necessary for server-side development. Thousands of regular downloads are made possible by npm's vast ecosystem of third-party packages. On the other hand, hackers are drawn to its popularity. In a supply chain attack, hackers take advantage of developers' reliance on these packages by inserting harmful code into their dependencies. These attacks have the potential to spread widely, affecting several operations.

Attackers do not only target files; they also go after apps, looking for security holes to exploit. Attacks such as Man-in-the-Middle (MITM), injection, and Cross-Site Scripting (XSS) can hack online applications, extract data, and change databases. Following best practices is critical to protect your apps and reduce the impact of these threats.

In this post, we will take a look at some different ways to make sure that Node.js apps are secure when used in production.

What Is The Significance of Node.js Security?

Applications built using Node.js are vulnerable to crimes because of how frequently they are exposed to the internet. Dependencies with security flaws can be introduced since it uses npm to rely on third-party packages. Data leaks, illegal access, and system compromises may all result from just one security hole.

Your app is still vulnerable to attacks regardless of whether npm vulnerabilities are present or not. Attacks like SQL injection, cross-site scripting (XSS), and data breaches can occur as a result of database interactions, user inputs, and external API requests, respectively. Your app's security might be jeopardized due to inadequate error handling, faulty authentication or authorization, unsafe setups, or bad session management.

Now, we'll talk about the best practices to keep Node.js apps safe in production in the following part.

Node.js Security: Best Practices

1. Add headers that ensure security

To make sure your app is secure, you must use security headers. Your website is open to a variety of attacks, including:

Cross-Site Scripting (XSS): Hackers steal user information or take over sessions by inserting malicious code.
Clickjacking: Contains hidden features that can trick users into doing unexpected actions when they click on them.
Injecting Content: Enables illegal changes to the content of the website.

Including security headers in your application can help reduce these threats. Using the Helmet library in conjunction with frameworks like Fastify or Express is a common choice. Helmet will take care of adding the required security headers automatically.

This is how you can use Helmet with Express:

`import helmet from "helmet";
app.use(helmet());

You can use the following Fastify-Helmet plugin in your Fastify project:

import helmet from "@fastify/helmet";

fastify.register(
helmet,
{ contentSecurityPolicy: false } // Example: disable the contentSecurityPolicy middleware
);`

Your application will automatically receive the required security headers. To guarantee encrypted connections, for example, the Strict-Transport-Security header tells browsers to choose HTTPS. In order to prevent cross-origin attacks, the Cross-Origin-Resource-Policy header prevents other websites from acquiring your resources. The Helmet documentation has a comprehensive list of the security headers that the package adds.

2. Maintain up-to-date versions of Node.js and its dependencies.

It is important to update Node.js and your dependencies on a regular basis to keep your environment secure. There have been vulnerabilities in NPM, but the maintainers are constantly fixing them. Maintaining an up-to-date system secures your application against previously discovered vulnerabilities and guarantees that you get the most recent fixes and enhancements.

Utilizing nvm is a time-saving method for updating Node.js. The steps to update to the latest version are as follows:

`nvm install

nvm use `

Your development and production settings will remain constant if you automate this step in your CI/CD pipeline.

Use techniques like npm-audit or Snyk to verify the security of the packages you use.

By comparing your project's dependencies to the GitHub Advisory Database, the npm-audit script can find security flaws and possible solutions:

npm audit

You will be notified of what needs to be done if vulnerabilities are found:

Output
23 vulnerabilities (5 moderate, 18 high)

To address all issues, run:
npm audit fix

When you run npm audit fix, it is going to replace any vulnerable dependencies with suitable updates, making sure they are reliable and fresh.

Consider using third-party tools like as Snyk for a more thorough investigation. These tools offer greater insights into potential vulnerabilities:

`npx snyk test

npx snyk wizard`

It is also important to regularly update your packages to the latest releases. Identifying and updating packages to new versions is made easy using the npm-check-updates tool:

npx npm-check-updates

Using the -u option will update the package.json file to the most recent versions:

npx npm-check-updates -u

To understand potential implications, study changelogs for big changes regularly while updating. Always use a staging environment to test changes before deploying them to production. In addition, it is critical to have a plan B in place in case changes lead to problems.

3. Identify the factors that should be tracked

The key to a successful Node.js application monitoring is knowing what needs to be observed. The correct metrics can be traced and tracked, and data about your NodeJS application's performance can be collected in this way.

It is just as important to know which logs to manage as it is to keep an eye on latency, error rates, throughput, and services. If you want your monitoring exercise to be effective, you need to identify the crucial occurrences.

4. Configure alerts based on urgency

Creating alerts using the monitoring tool's notification system is easy; configuring alerts for critical and urgent metrics is another matter altogether. You can find critical events that might degrade your application's availability and performance with the aid of a dynamic alert setting. You can resolve any problem before it spirals out of hand with this method.

Notifications can be categorized as either low-priority or high-priority. Priority events must be attended to and addressed, whereas low-priority notifications can be ignored for later consideration.

5. Make sure the right person gets the notifications

Be careful to include the right people in your team when you configure notifications for important and urgent occurrences. Those whose department or expertise handles the specific issue or issues should be specifically alerted.

6. Protect confidential data

If your database stores private data like emails and credentials in text form, hackers can easily access them when your application has an authentication capability. In addition, while comparing passwords or other sensitive information, attackers might take advantage of your application's response latency. They can determine the length and worth of passwords by tracking response times via repeated queries.

Hardcoded secrets, such as database names, passwords, or secret keys from other providers (like AWS), can also pose a serious security risk in your application code. Your secrets will be revealed if someone gets their hands on your source code. The difficulty of rotating or updating secrets due to hardcoding makes it riskier to update or alter the code without redeploying it.

Strong authentication procedures can be implemented in your Node.js application using packages like Passport.js or OAuth to prevent these security flaws. To lessen the likelihood of unwanted access, you might want to consider using multi-factor authentication (MFA). To make sure that people establish secure passwords, implement stringent criteria. All of the characters in the string must be at least 12 characters long and contain both uppercase and lowercase letters, numbers, and symbols.

Also, never keep critical information (such as a password) in plain text. Use robust, one-way hashing algorithms instead; you may get them in packages like Argon2 or bcrypt. It is not feasible to conduct brute-force assaults on these methods due to their computationally demanding architecture.

To hash passwords in Node.js, follow these steps:
import bcrypt from "bcrypt";
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

Keep in mind that there are timing attacks that can be used to guess password lengths or values by taking advantage of discrepancies in response times. When verifying sensitive data, such as passwords, use constant-time comparison methods.

For comparison, the built-in crypto library has the timingSafeEqual method:
import crypto from "crypto";

const safeCompare = (a, b) =>
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));

After learning about robust authentication and encryption, the next step in security is to look at error management and logging.

7. Error handling and logging

While running in production, mistakes might happen in your application, databases, or servers. Users can observe error messages that show important information about your system if they are not handled properly. These messages may include stack traces, inputs that do not match, or network timeouts. If your application doesn't properly handle errors, it might leave it vulnerable to attackers who could discover the versions of frameworks and dependencies it utilizes.

Furthermore, if exceptions are not appropriately controlled, attackers can gain knowledge of your system components, including APIs, and be able to circumvent security measures. An attacker might get past your login procedure if, for example, your website just checks for valid credentials and doesn't handle unusual or wrong inputs.

Inadequate recordkeeping might also make problem detection and diagnosis more challenging. If you want to spot unusual behavior or figure out how the attack unfolded, you need logs. You might not know an attack has happened or have trouble understanding how it happened if you don't have logs.

Sending unexpected data or pushing your application to extreme situations might help you address these problems during testing. Add error handling using the try/catch construct:

try {
// Code that may throw an error
} catch (error) {
// Handle the error
}

Make sure you deal with promises correctly while doing asynchronous activities. Direct exceptions to the appropriate event when using EventEmitters in asynchronous methods:

emitter.on("error", (err) => {
console.error("An error occurred:", err);
});

To get more accurate insights, you should incorporate logging with a tool like Pino. It can record important occurrences. Some examples are:

Not all inputs are valid.
Attempts at authentication, especially those that failed.
Access control failures.
Anything that seems like it was tampered with, including sudden modifications to state data.
Connecting attempts are made using session tokens that are either expired or invalid.
It tries to establish a connection using session tokens that are either not valid or have expired.

Make sure that any error messages you send out are generic and don't contain any personal information or passwords. Attackers will be unable to erase evidence of their activities thanks to a centralized logging system such as Better Stack.

8. Secure your app against denial-of-service attacks

The security of your Node.js application depends on your ability to prevent Denial of Service (DoS) attacks. It calls for an all-encompassing strategy that employs several security layers and handles different attack vectors. Multiple types of denial-of-service attacks are possible, including those that target the network layer (e.g., SYN floods), the application layer (e.g., HTTP floods), or the resource depletion tactics (e.g., CPU, memory, or disk space).

Applying rate limitation is a crucial step in protecting your application. Packages like rate-limiter-flexible and express-rate-limit make this possible in Node.js applications, with the former being more suited to complicated circumstances and the latter to simpler ones. Nginx, cloud load balancers, and API gateways are some of the infrastructure-level solutions that can be considered for rate limitation, which can provide even more powerful protection.

Your server can be protected from attackers that try to overwhelm it with massive amounts of data by limiting the size of the payloads that are sent in with requests. This can be set up in web frameworks' built-in middleware or on the firewall itself:

app.use(express.json({limit: '2mb'}));
app.use(express.urlencoded({limit: '2mb', extended: false}));

By allocating a separate heap for private information, you can protect your application from memory-based attacks with the --secure-heap=n option in Node.js. These steps won't be enough to protect your application against denial-of-service attacks. You should additionally employ effective caching strategies, use Content Delivery Networks (CDNs), and perform stringent input validation.

9. Make use of linter plugins concerning security

You can greatly boost the security of your code by using linter plugins such as eslint-plugin-security. By identifying problems like unsafe Regular Expressions, incorrect input validation, and the potentially dangerous usage of eval(), this plugin aids in finding vulnerabilities early on in the development process. If you want to make sure your codebase stays safe all the way through development, try combining these linting rules with git hooks. This will enforce security checks before code is committed, making them even more effective.

10. Import built-in modules using the 'node:' protocol:

The node: protocol should be employed exclusively when importing pre-existing Node.js modules, as follows:

import { methodName } from "node:module";

As a result, you won't have to worry about typosquatting attacks, in which attackers attempt to get you to use a compromised package by making it seem like a real one. To avoid accidentally importing malicious third-party packages, you may import only official Node.js modules by using the node: protocol.

11. Use non-root users in Docker containers:

A security risk exists in Docker systems due to the prevalence of containers operating with root capabilities by default. To get around this, make a user that isn't root and execute your program as that user. Follow this:

# Dockerfile example
...
EXPOSE 3000
USER node
...

Also, be sure that no one other than non-root users has any kind of active involvement on your servers. Since the non-root user would have limited rights, the consequences of any security breaches would be limited.

12. Prevent SQL injection with parameterized queries

Using parameterized queries, which securely encapsulate user inputs, can help reduce the risk of SQL injection, a prevalent attack vector. Consider utilizing Object-Relational Mappers (ORMs) such as Sequelize, Knex, TypeORM, or Objection.js to further mitigate the danger of SQL injection. By hiding the underlying SQL queries and automatically implementing safeguards against SQL injection, these ORMs offer built-in techniques for secure database interaction. Finding memory vulnerabilities is another possible application of SQLmap.

Final Remarks

You can be certain that your Node.js apps will function securely and vulnerability-free in production if you follow the guidelines provided in this article. Due to the extensive nature of the Node.js framework, developers using it for microservice development have the difficulty of constant monitoring. The most effective approach to identify and comprehend the performance bottlenecks impacting your Node.js application is to have a good grasp of what needs monitoring and to put best practices into practice.

Top comments (0)