Node.JS Security Best Practices..

Twinkle GoyalTwinkle Goyal
8 min read

The security of an application is extremely important when we build a highly scalable and big project. So in this article, we are going to discuss some of the best practices that we need to follow in Node.js projects so that there are no security issues at a later point of time.

1. Keep your NPM libraries Up To Date

2. Harden your HTTP cookies

3. Set the Security HTTP Headers

4. Rate Limiting / Authentication Limits / Payload Size Limit

5. Password Encryption

6. JWT Blacklist

7. JSON Schema Validation

8. ORM / ODM against Injections

9. Leaking Server Information

1. Keep your NPM libraries Up To Date

NPM libraries make it easier and quicker to build a full-featured Node.js backend application. At the same time, they can also introduce security risks into your application. New vulnerabilities are discovered all the time, and it is the maintainer's job to Gopi Ram Pawan Kumar, Ward no. 14, Aggarssain Market, Sangaria 335063 them and release an updated version of the package. here is why you should keep your dependencies up to date.

To ensure that NPM libraries you're relying on are secure, you can use Audit and Snyk. These tools analyse your project's dependencies tree and provide insights into any known vulnerabilities.

Here's an example of running rpm 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.

2. Harden your HTTP cookies

HTTP cookies is a standardise sort of mechanism across browser's and server. They're nothing more than a way to store data sent by the server and send it along with the future request's. The server sends a cookie, which contains small bits of data. The browser stores it and sends it along with future requests to the same server.

Why would we bother about cookies from a security perspective? Because the data they contain is extremely sensitive. Cookies are generally used to store session IDs or access tokens, which is golden treasure for attacker's. Once they are exposed or compromised, attacker's can misuse it or take over their rights on your application.

Expires :- Specifies when a cookie should expire, so that browsers do not store and transmit it indefinitely. A clear example is a session ID, which usually expires after some time. This directive is expressed as a date in the form of Date: <day-name>, <day> <month> <year> <hour>;:<minute>:<second> GMT, like Date: Fri, 24 Aug 2018 04:33:00 GMT. Here’s a full example of a cookie that expires on the 1st of January 2018:

access_token=1234;Expires=Mon, 1st Jan 2018 00:00:00 GMT

Max-Age :- Similar to the Expires directive, Max-Age specifies the number of seconds until the cookie should expire. A cookie that should last 1 hour would look like the following:

access_token=1234;Max-Age=3600

HttpOnly :- As we’ve seen earlier in this series, XSS attacks allow a malicious user to execute arbitrary JavaScript on a page. Considering that you could read the contents of the cookie jar with a simple document.cookie, protecting our cookies from untrusted JavaScript access is a very important aspect of hardening cookies from a security standpoint.

Luckily, the HTTP spec took care of this with the HttpOnly flag. By using this directive we can instruct the browser not to share the cookie with JavaScript. The browser then removes the cookie from the window.cookie variable, making it impossible to access the cookie via JavaScript.

Secure :- The Secure flag specifies that the cookie may only be transmitted using HTTPS connections (SSL/TLS encryption) and never sent in clear text. If the cookie is set with the Secure flag and the browser sends a subsequent request using the HTTP protocol, the web page will not send this cookie to the web server in its HTTP response.

The Secure attribute is meant to protect against man-in-the-middle (MITM) attacks. However, it protects only the confidentiality of the cookie, not its integrity. In a MITM attack, an attacker located between the browser and the server will not receive the cookie from the server via an unencrypted connection but can still send a forged cookie to the server in plain text.

Note that you can only set the Secure flag when using a secure connection.

3. Set the Security HTTP Headers

The default HTTP headers in Express are not very secure. 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.

4. Rate Limiting / Authentication Limits / Payload Size Limit

Here, Request entity is too large. This error occurs when the incoming request data (typically in the request body) exceeds the maximum size limit set by your Node.js server.

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.

Denial Of Service (DDoS) attack, here attacker's try to overload your server with sending excessively large requests, causing it to crash or became unresponsive to legitimate user.

Brute Force, here attacker's uses hacking method that uses trial and error to crack passwords, login credentials, and encryption keys. It is a simple yet reliable tactic for gaining unauthorised access to individual accounts and organisation's' systems and networks.

Resource Exhaustion, here large requests can consume a significant amount of memory and processing power, which potentially impacting the performance of your application for other users.

Limiting request size in Node.js can achieve by using body-parser or in-built express.json().

5. Password Encryption

Password encryption is essential to store user credentials stored in a database securely. Without password encryption, anyone accessing a user database on a company's servers (including hackers) could easily view any stored passwords. If someone can read your password on a server, they can use it simply by copy/pasting it—no matter how long or complicated the password might be!

Encryption scrambles your password before saving it on the server. So, if someone hacks the server, instead of finding password123, they find a random series of letters and numbers.

You can use bcryptjs library to encrypt your password. Below this code, we are going to be creating the hashed version of this password by using bcrypt .hash() method. This method returns a promise containing the hashed version of our password and we would await it's value and store it in a variable called hashedPassword.

const bcrypt = require("bcryptjs")

const myFunction = async () => {
    const password = "John2468$"
    const hashedPassword = await bcrypt.hash(password, 8)
    console.log(password);
    console.log(hashedPassword);
}

myFunction()

We would create a variable called isMatch and we would use the .compare() method on bcrypt and then pass in the required values, i.e user input and the hashed version we have stored.

Now, we can type the code to get this done, See the code below:

const bcrypt = require("bcryptjs")

const myFunction = async () => {
    const password = "John2468$"
    const hashedPassword = await bcrypt.hash(password, 8)
    console.log(password);
    console.log(hashedPassword);

    const isMatch = await bcrypt.compare("John2468$", hashedPassword);
    console.log(isMatch);
}

myFunction()

When we run our file again, we should get the boolean called true printing.

6. JWT Blacklist

One reason you would need to invalidate a token is when you're creating a logout system, and JWT is used as your authentication method. Creating a blacklist is one of the various ways to invalidate a token. The logic behind it is straight forward and easy to understand and implement.

A JWT can still be valid even after it has been deleted from the client, depending on the expiration date of the token. So, invalidating it makes sure it's not being used again for authentication purposes.
If the lifetime of the token is short, it might not be an issue. All the same, you can still create a blacklist if you wish.

We can store all generated JWT tokens in db or file, before request process we can put check if it still exist in db or file, means we can process the request. If it is removed / compromised we'll not process the request further and ask user to login again.

Also, we can use the mechanism of access and refresh token.

7. JSON Schema Validation

JSON schema, which is designed to be fast and simple to use. It is something like defining schema for request body ( something like typescript for http request body )

8. ORM / ODM against Injections

These are the extra layers which sit between the Node.js server and databases you use.

The problem they solve is allowing developer to enforce a specific schema at the application layer. These also provide hooks, model validation and other features.

ORM ( Object Relational Mapping ), used for relational databases like Oracle, PostgreSQL, MySQL etc by using commonly used in industry package named as Sequelize.

ODM ( Object Document Mapper ), which maps objects with a Document Database like MongoDB by using commonly used in industry package named as Mongoose.

9. Leaking Server Information

If attacker's is get to know about your server information so there're chances they can attack on your server and create vulnerabilities.

So, there are few things to take care :-

disable("x-powered-by"), to avoid sending server info in http response header.

Conclusion

This is one of the various key security practices for secure Node.JS apps and APIs.

Thank you for reading, cheers.

1
Subscribe to my newsletter

Read articles from Twinkle Goyal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Twinkle Goyal
Twinkle Goyal