NodeJS Security Best Practices
Mohammad Faisal
Posted on February 19, 2024
To read more articles like this, visit my blog
Today, we will see how we can improve the security of a NodeJS application. There are lots of aspects to security in the NodeJS application. We will get familiar with many of the concepts and see how to prevent unwanted attacks on our application.
Nothing is bulletproof, but being safe doesn't hurt.
The most popular one!
We will first use an excellent npm package named helmet.
It sets up various HTTP headers to prevent attacks like Cross-Site-Scripting(XSS), clickjacking, etc.
yarn add helmet
Then use it inside your index.ts
import helmet from "helmet";
app.use(helmet());
That's it! You don't need to do anything else!
You should also take a look at helmet-csp
Prevent DOS attack
DOS means Denial of Service. If an attacker tries to swamp your server with requests, our real users can feel the pain of slow response time.
To prevent this, we can use an excellent package named toobusy-js
This will monitor the event loop, and we can define a lag parameter that will monitor the lag of the event loop and indicate if our event loop is too busy to serve requests right now.
yarn add toobusy-js
Then add a new middleware to indicate that the server is too busy.
import too busy from 'toobusy-js';
app.use(function (req, res, next) {
if (toobusy()) {
res.send(503, 'Server too busy!');
} else {
next();
}
});
Rate Limiting
Rate limiting helps your application from brute-force attacks. This allows to prevent the server from being throttled.
Unauthorized users can perform any number of requests with malicious intent, and you can control that with rate-limiting.
For example, you can allow a user to make five requests per 15 minutes for creating an account.
Or you can allow unsubscribed users to make requests at a certain rate limit. something like 100requests/day
There is a nice package named express-rate-limit. First, install it
yarn add express-rate-limit
Then create a rate-limiting configuration for it.
import rateLimit from "express-rate-limit";
export default rateLimiter = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 24 hrs in milliseconds
max: 100, // maximum number of request inside a window
message: "You have exceeded the 100 requests in 24 hrs limit!", // the message when they exceed limit
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
const app = express();
app.use(rateLimiter);
This will allow you to add a rate limit for all of your routes. You can also just add rate-limiting for specific routes.
But if you are behind a proxy. This is the case for most cases when you use any cloud provider like Heroku aws etc., then the IP of the request is basically the IP of the proxy, which makes it look like that request is coming from a single source, and the server gets clogged up pretty quick.
To resolve this issue, you can find out the numberOfProxies between you and the server and set that count right after you create the express application.
const numberOfProxies = 1;
const app = express();
app.set("trust proxy", numberOfProxies);
To learn more about trust proxy refer to the documentation
Configure Cors
CORS will keep your application safe from malicious attacks from unknown sources
It's really easy to configure in nodejs.
npm i cors
then use it inside the index.ts file
import cors from "cors";
let corsOptions = {
origin: "http://example.com",
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
};
app.use(cors());
Prevent XSS attacks
XSS attack means cross-site scripting attacks. It injects malicious scripts into your application.
An attacker can use XSS to send a malicious script to an unsuspecting user. The end user's browser has no way to know that the script should not be trusted and will execute the script. Because it thinks the script came from a trusted source, the malicious script can access any cookies, session tokens, or other sensitive information retained by the browser and used with that site.
You can protect your application by using xss-clean
yarn add xss-clean
Then use it inside the index.ts
file
import xss from "xss-clean";
app.use(xss());
Prevent SQL Query injection attacks
If you use user input without checking them, a user can pass unexpected data and fundamentally change your SQL queries.
For example, if your code looks like this.
UPDATE users
SET first_name="' + req.body.first_name + '" WHERE id=1001;
If a user types his first name as
Bobby", last_name="Egg"; --
Then the query becomes
UPDATE users
SET first_name="Bobby", last_name="Egg"; --" WHERE id=1001;
So all your users are named Bobby Egg which is not cool.
If you use Sequalize, TypeORM or for MongoDB, we have Mongoose these types of ORM tools, then you are safe by default because these help us against the SQL query injection attacks by default.
If you don't want to use ORM then there are some other packages as well!
For PostgreSQL we have node-postgres
Limit the size of the body of the request
Using body-parser you can set the limit on the size of the payload
npm i body-parser
By default, body-parser is configured to allow 100kb payloads size. You can set the limit like the following.
import bodyParser from "body-parser";
app.use(bodyParser.json({ limit: "50kb" }));
app.use(bodyParser.urlencoded({ extended: true }));
Use linter
A linter can force you to follow these best practices by default. You can use eslint-plugin-security for that.
yarn add -D eslint-plugin-security
And inside your .eslintrc file
"extends": ["plugin:@typescript-eslint/recommended", "plugin:security/recommended"],
Enforce HTTPS
It would be best if you always use HTTPS over HTTP when possible.
yarn add hsts
Then use it inside your index.ts
import hsts from "hsts";
app.use(
hsts({
maxAge: 15552000, // 180 days in seconds
})
);
// Strict-Transport-Security: max-age: 15552000; includeSubDomains
Use CSRF Protection middleware
To learn more about CSRF. Go here
Consider using csurf
import csrf from "csurf";
var csrfProtection = csrf({ cookie: true });
app.get("/form", csrfProtection, function (req, res) {
// generate and pass the csrfToken to the view
res.render("send", { csrfToken: req.csrfToken() });
});
This is not required for an application that doesn't handle any form of data.
Validate Incoming Requests
You should consider validating the incoming requests to your application to check if they contain any malicious data. There are many ways to do this, but the most popular way is to use a schema validation library like Joi or class-validator.
You can refer to the following article for more information.
https://www.mohammadfaisal.dev/blog/request-validation-nodejs-express
Validating Output
You want to secure the incoming data instead you should try to validate any data that you give output.
For example, if you render to the frontend using NodeJS, then this becomes an issue.
Let's say our user generates the following data for showing in the browser later.
<script>alert('I am not sanitized!');</script>
This is harmless, but if you don't sanitize it before giving it back to the javascript frontend, then it can cause trouble.
To avoid this, we have several options.
html-escape
// if you need to render html output
xml-escape
// if you need to give xml output
shell-escape
// for shell commands
These libraries can help you to sanitize the output as well.
Using Hash for Sensitive Information
Storing passwords in plain text was never a good idea. You should instead create a has of your password.
A Hash is a fixed-sized string that we can generate from any string, and it's not decryptable by design. You can only produce it again if the same input text is given again.
So while storing passwords, you will create a hash of your password and store it in the database. This way, even if our database is exposed, the passwords will not be exposed.
Using the Salt with Hash
There is a problem with the Hashing mechanism because two identical texts will always generate the same hash. As most people use very common passwords, this technique is vulnerable to exploitation.
To avoid this issue, we use a technique called salt. It's basically a random string that is appended to the given input. So even if you give two identical inputs, the outputs will not be the same.
Hide errors from end-users
It would be best if you did not expose errors to your users. In order to do that, you will need to handle production environment error handling seriously. Following is a good guide on where you can start.
https://www.mohammadfaisal.dev/blog/error-handling-nodejs-express
Load secrets securely
Be extra careful about loading secrets into your application. You must not include secrets as plain strings in the application. Using some kind of environment file is necessary to achieve this. You can refer to the following example for that.
https://www.mohammadfaisal.dev/blog/nodejs-environment-handling
Compression
Compression is a technique that can reduce the size of the static file and JSON response.
In nodejs, that we can do with a nice middleware package named compression
First, install it
yarn add compression
Then add it inside your index.ts
import compression from "compression";
app.use(compression());
And that's it! There are other options that you can use. Refer to the documentation for that.
Some more resources:
https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
https://medium.com/@nodepractices/were-under-attack-23-node-js-security-best-practices-e33c146cb87d
Github Repo
https://github.com/Mohammad-Faisal/nodejs-security-best-practices
Have something to say? Get in touch with me via LinkedIn or Personal Website
Posted on February 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.