Hack-Proof Your Node.js APIs: A Practical Guide against Cyber Threats
While building real world applications for clients the security of an application is extremely important. There are a lot of developers who do not focus on making their application secure. Here, I am not going to tell the whole about security as I am not application security expert. But being a developer there are some essential best practices that every developer must follow to ensure the security of their application. In this article, I will not only explain you the best practices but also let you know how you can integrate best security practices in your Node.js application.
Bonus: At the end of this article, I will also show you the complete way of securing the authentication and authorization cycle with JWT which most of the tutorial out there do not perform. Believe me its really crucial when it comes to security of an application.
Disclaimer: This article is generic about security of your web application. No matter in which programming language you are building your APIs. In my case, I will implement the security practices using Node.js (Express.js) and Database that I am going to use is MongoDB. The concept would be same for other backend technologies, only the implementation would be different.
Compromised Database:
Lets start with Compromised database. As its name suggest, its a database that has compromised in some aspect of storing data which in actual should not be. Your database should never ever store sensitive data/information in the form of plain text. Your information must be encrypted.
For Example: In case of you are storing passwords in you database you can encrypted them using bcrypt and for reset or other tokens can use SHA256 Algorithm to encrypt them before saving into you database.
Brute Force Attack:
Brute for attack is a trial and error attack done by the Hackers to crack passwords, login credentials, and encryption keys. It is a simple and reliable technique for gaining unauthorized access to individual accounts and networks. In this attack the hacker simply try or loop millions of credentials to access the account.
Preventions:
- You can use Bcrypt to make the login request process slow. As in bcrypt when a user logs in. His credentials first get encrypted and then compared with an encrypted version in database. This whole process takes time that will slower the request of attacker or might crash his request.
- The second best technique to secure your application from Brute force attack is to implement rate limiters such as allowing users to try login for 10 times e.g, if a user tried to access his account with wrong credentials 10 times then he will have to wait for 1hr or so, to try login again. This will break the brute force attack loop.
Cross-site Scripting Attack:
In this attack the hacker tries to run his malicious code on our page. This can be done if user accidentally open some link which is not safe. In this way the hacker will get access to the local storage. In local storage, there may be our JWT token which can be get access by the attacker.
Preventions:
- Store your JWT in httpOnly cookie only instead of local storage. So that browser can only send and receive it cannot access or modify it.
- Always Senitize user data. Never trust on your user. Do not get data as it is from the user always santize it. In simple only allow the data that is necessary, ignore rest of the data coming from the user.
- Set HTTP special headers so it make harder from the attacker to attack.
DOS (Denial of Service) Attack
In this attack the hacker sends so many requests to the server that the server got crashed and application becomes unavailable.
:
- Again here you can Implement rate-limiters to avoid bunch of request coming from the attacker.
- You can also limit the data that can be send in body or post request. Limit data in terms of size etc.
NoSQL Query Injection Attack
This attack happens when attacker instead of injecting valid data he injects query to create query expressions that are going to translate to true to get logged in without providing a valid password.
For example: we can login to an account without knowing the credentials. The query works something like by any means for instance you have get a password but you do not know to which user this password belongs to. In such case the attacker make a query for username such that it is going to be true. In such case the attacker will be logged in and will get access to the data. This can be vice versa.
Preventions:
- Create a well defined schemas using mongoose in case you are using MongoDB.
- Do not trust on the user input. Always sanitize the data coming from the user.
Implementation
Sending JWT in httpOnly Cookie
Cookie is nothing simply a text that browser used to send and receive for the future requests
// In authenticationController.js filer
// Generating token
const signToken = (id) => {
return jwt.sign({ id: id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
};
const createSendToken = (user, statusCode, res) => {
const token = signToken(user._id);
const cookieOptions = {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000,
),
httpOnly: true, //so browser cannot access or modify it
};
if (process.env.NODE_ENV === 'production') cookieOptions.secure = true;
res.cookie('jwt', token, cookieOptions);
res.status(statusCode).json({
status: 'success',
token: token,
data: {
user: user,
},
});
};
This is how you create and save your token in cookies in Node.js. Here you have noticed that in case of ‘Production’ environment we are setting our secure option in cookieOptions Object as true. The reason behind this fact is that your request must be through HTTPs (secure). So that the browser can only send and receive the JWT token cannot access or modify it.
Implementing Rate limiters
For implementing rate limiter I am using a package called ‘express-rate-limit’. To prevent Brute force attacks and denial of service attack etc.
We will implement this logic on the top of our app as a global middleware function. In my case its app.js underwhich all application is running. So I am going to implement rate limiter middleware here. Actually its very easy.
// In app.js
const rateLimit = require(‘express-rate-limit’);
const limiter = rateLimit({
max: 100, //maximum allowed requests
windowMs: 60 * 60 * 1000, //time in miliseconds
message: 'Too many requests from this IP, please try again in an hour!',
});
app.use('/api', limiter);
This middleware will be called for the routes starting with ‘/api’ as in my case all routes are starting within ‘/api’. You can implement according to the staring keyword for your routes.
Setting security headers
To prevent cross site scripting attacks we sanitize the data coming from the user and use special headers to make the attack harder.
I am going to use npm package called “helmet” to enable security headers
// In app.js
const helmet = require('helmet');
app.use(helmet());
Again place this middleware on the top of every middleware. So that this middleware add headers to all URLs and API requests. As mentioned above, in my case the top of my application is app.js. So, I am implementing this in “app.js”.
When you will make http request you will see that this middleware has added some extra security headers which will prevent XSS (cross site scripting) attacks.
Data sanitization
As I have mentioned above to prevent cross-site scripting attacks and NoSQL injection attacks always sanitize the data coming from the user.
// In app.js
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
// Data sanitization against NoSQL query middleware
app.use(mongoSanitize());
// this above middleware looks for req.body, req.params and req.query strings all dollar signs and dots
// Data sanitization against XSS
app.use(xss());
// this will sanitize the malicious html code
Again I have implemented this middleware in the top of my application so that all APIs get secured with these middleware.
Preventing HTTP Parameter pollution
HTTP Parameter Pollution (HPP) is an attack evasion technique that allows an attacker to create an HTTP request to manipulate or retrieve hidden information. To prevent this I am going to use ‘hpp’ module.
//In app.js
const hpp = require('hpp');
// Data sanitization against XSS
//Preventing params pollution
app.use(
hpp({
whitelist: [
'duration',
'ratingsQuantity',
'ratingsAverage',
'maxGroupSize',
'difficulty',
'price',
],
}),
);
here you might be wondering what is “whiteList ”here so let me explain that so if I do a query such as “api/v1/tours?sort=duration&sort=price.”
Here, I am sorting my tours data with duration and price here we will get an error as we are passing “sort” two times as it is our requirement. Here “hpp” module will allow the second method to work such as in this case the sort will be in terms of price. The “sort=duration” will be ignored as sort is coming twice. But due to “hpp” module we are at least not getting any error. But there are some cases where we really want twice query of the same property as in this case. So for that case we provide a white list. All properties which are in whiteList will be allowed despite they are twice.
Other Best Practices
- In production always always use HTTPs otherwise your queries and information will be revealed.
- Always create random password reset token with expiry dates. Most of the developers just simply create random tokens manually. Instead you can create a 16 or 32 bytes strong token using crypto module as below:
- Deny access to JWT after the password is changed. The new JWT must be generated and user must have to login in again.
- Never commit ‘config.env’ file you git repository. As your env file contains sensitive information and is used to avoid adding sensitive information in the form of plain text in your application code.
- Do not send error details to the clients like ‘stack trace error’ . This might leak sensitive information about your files.
- To prevent ‘Cross site request Forgery’ use ‘csurf’ package (Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated)
- Try to require re-authentication before high value actions such making payments or deleting something.
- Implement a blacklist of untrusted tokens. For instance, there may be a case an authenticated user might perform a malicious activity so in such case we cannot log out the user. But we can implement a list of untrusted tokens which must be validate before any query process.
- Confirm users email addresses before creating an account. Like sending a token to the provided email to confirm the user.
- Keep users logged in user with fresh tokens. Your tokens must have expiry dates.
- Implement two factor authentication. Like after logging in sending an OTP or code to the text or email.
Complete Authentication and Authorization cycle with JWT
So, As I had said there would be a bonus section at the end of this article. So here you go.
In most of the tutorials out there developers create authentication system using JWT till the login and signup. So what if once the user have logged in and he updated or changed his password or credentials. Shouldn’t his JWT token be expired and make the user to log in again with new updated credentials? The answer is YES, this must be implemented. As there could be a case that the same account has logged in two devices. And the user might suspect that by any means some one has got access to his password and his account has logged-in in some other device. Then the best common practice he can do is to change his credentials immediately. On changing his credentials the JWT token should get expired and logout the user from all devices. In this way he can secure his account. The expiration of JWT token on updating credentials and navigate the user to the login route again is missing in most of authentication and authorization system implemented by the developers. Let me show you how can you implement this missing part. I am assuming here that you have implemented the traditional JWT authentication system and only you have missing the above explained step. So lets do that:
There are two steps, One implement this on your protect route Function and second, implement his on your resetCredentials or updatePassword route.
I have implemented all this logic in authenticationController.js
exports.protect = catchAsync(async (req, res, next) => {
// 1) Getting token and check of it's there
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer'))
{
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return next(new AppError('You are not logged in! Please log in to get access.', 401));
}
// 2) Verification token
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
// 3) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next(
new AppError(
'The user belonging to this token does no longer exist.',401));
}
// 4) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next(
new AppError('User recently changed password! Please log in again.', 401)
);
}
// GRANT ACCESS TO PROTECTED ROUTE
req.user = currentUser;
next();
});
Here as you can see in the 4th step “Check if user changed password after the token was issued” we are verifying that password has updated or not. In case of yes, we have raised an error and made user to login again.
exports.resetPassword = catchAsync(async (req, res, next) => {
// 1) Get user based on the token
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex');
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() }
});
// 2) If token has not expired, and there is user, set the new password
if (!user) {
return next(new AppError('Token is invalid or has expired', 400));
}
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
// 3) Update changedPasswordAt property for the user
// 4) Log the user in, send JWT
createSendToken(user, 200, res);
});
exports.updatePassword = catchAsync(async (req, res, next) => {
// 1) Get user from collection
const user = await User.findById(req.user.id).select('+password');
// 2) Check if POSTed current password is correct
if (!(await user.correctPassword(req.body.passwordCurrent, user.password))) {
return next(new AppError('Your current password is wrong.', 401));
}
// 3) If so, update password
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
await user.save();
// 4) Log user in, send JWT
createSendToken(user, 200, res);
});
Here in both controller functions (resetPassword and updatePassword) credentials are changing or in both cases we have called “createSendToken” function. This function generates a new token and store it into HttpOnly Cookie.
Conclusion
Securing Node.js APIs is very crucial aspect against cyber threats. So uptil now, we explored essential security practices every developer should implement, regardless of the programming language used for API development. From protecting against compromised databases to thwarting brute force attacks, cross-site scripting, denial of service attacks, and NoSQL query injections, we delved into practical strategies to fortify your applications.
Always Remember, security is an ongoing process, and staying vigilant against emerging threats is crucial for maintaining the integrity of your Node.js APIs.
Thank you so much for reading this far. I hope you find this article informative and helpful. If yes, then share it with your friends. If you want to keep yourself up to date with hot technologies and the industries latest trends, and want to grow along with my coding journey then follow me on X/Twitter and connect with me on LinkedIn. I post a lot of informative and learning content related to programming and latest technologies.