The one thing that developers tend to considers at the end of the development cycle is the “security” of the application. A secure application is not a luxury, it’s a necessity. You should consider the security of your application at every phase of the development such as architecture, design, code, and finally the deployment.
In this tutorial, we are going to learn ways to secure our Node.js application. Let’s dive in.
Data Validation – Never Trust Your Users
You must always validate or sanitize the data coming from the user or other entity of the system. The bad validation or no validation at all is a threat to the working system and can lead to a security exploit. You should also escape the output. Let's learn how to validate the incoming data in Node.js. You can use a node module called validator to perform the data validation. For example.
const validator = require('validator');
validator.isEmail('foo@bar.com'); //=> true
validator.isEmail('bar.com'); //=> false
You can also use a module called joi (recommended by Codeforgeek) to perform the data/schema validation. For example.
const joi = require('joi');
try {
const schema = joi.object().keys({
name: joi.string().min(3).max(45).required(),
email: joi.string().email().required(),
password: joi.string().min(6).max(20).required()
});
const dataToValidate = {
name: "Shahid",
email: "abc.com",
password: "123456",
}
const result = schema.validate(dataToValidate);
if (result.error) {
throw result.error.details[0].message;
}
} catch (e) {
console.log(e);
}
SQL Injection Attack
SQL injection is an exploit where malicious users can pass unexpected data and change the SQL queries. Let's understand with the example. Assume your SQL query looks like this:
UPDATE users
SET first_name="' + req.body.first_name + '" WHERE id=1332;
In a normal scenario, you would expect that this query will look like this:
UPDATE users
SET first_name = "John" WHERE id = 1332;
Now, if someone passes the first_name as the value shown below:
John", last_name="Wick"; --
Then, your SQL query will look like this:
UPDATE users
SET first_name="John", last_name="Wick"; --" WHERE id=1001;
If you observe, the WHERE condition is commented out and now the query will update the users table and sets every user’s first name as “John” and last name as “Wick”. This will eventually lead to system failure and if your database has no backup, then you’re doomed.
How to prevent SQL Injection attack
The most useful way to prevent SQL injection attacks is to sanitize input data. You can either validate every single input or validate using parameter binding. Parameter binding is mostly used by the developers as it offers efficiency and security. If you are using a popular ORM such as sequelize, hibernate, etc then they already provide the functions to validate and sanitize your data. If you are using database modules other than ORM such as mysql for Node, you can use the escaping methods provided by the module. Let's learn by example. The codebase shown below is using mysql module for Node.
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
});
connection.connect();
connection.query(
'UPDATE users SET ?? = ? WHERE ?? = ?',
['first_name',req.body.first_name, ,'id',1001],
function(err, result) {
//...
});
The double question mark is replaced with the field name and the single question mark is replaced with the value. This will make sure that input is safe. You can also use a stored procedure to increase the level of security but due to lack of maintainability developers tend to avoid using stored procedures. You should also perform the server-side data validation. I do not recommend you to validate each field manually, you can use modules like joi.
Typecasting
JavaScript is a dynamic typed language i.e a value can be of any type. You can use the typecasting method to verify the type of data so that only the intended type of value should go into the database. For example, a user ID can only accept the number, there should be typecasting to ensure that the user ID should only be a number. For example, let's refer to the code we shown above.
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
});
connection.connect();
connection.query(
'UPDATE users SET ?? = ? WHERE ?? = ?',
['first_name',req.body.first_name, ,'id',Number(req.body.ID)],
function(err, result) {
//...
});
Did you notice the change? We used Number(req.body.ID) to ensure that ID is always the number. You can refer to this beautiful article by a fellow blogger to understand typecasting in depth.
Application Authentication and Authorization
Sensitive data such as passwords should be stored in the system in a secure way that malicious users don't misuse sensitive information. In this section, we will learn how to store and manage passwords which are quite generic, and pretty much every application has passwords in some way in their system.
Password Hashing
Hashing is a function that generates a fixed-size string from input. The output from the hashing function cannot be decrypted hence it's "one-way" in nature. For data such as passwords, you must always use hashing algorithms to generate a hash version of the input password string which is a plaintext string.
You might be wondering that if the hash is a one-way string then how come attackers gain access to passwords?
Well, as I mentioned above, hashing takes an input string and generates a fixed-length output. So attackers take a reverse approach and they generate the hashes from the general password list, then they compare the hash with the hashes in your system to find the password. This attack is called lookup tables attack.
This is the reason why you as an architect of the system must not allow generic used passwords in your system. To overcome this attack, you can something called "salt". Salt is attached to the password hash to make it unique irrespective of the input. Salt has to be generated securely and randomly so that it is not predictable. The Hashing algorithm we suggest you is BCrypt. At the time of writing this article, Bcrypt has not been exploited and considered cryptographically secure. In Node.js, you can use bcyrpt node module to perform the hashing.
Please refer to the example code below.
const bcrypt = require('bcrypt');
const saltRounds = 10;
const password = "Some-Password@2020";
bcrypt.hash(
password,
saltRounds,
(err, passwordHash) => {
//we will just print it to the console for now
//you should store it somewhere and never logs or print it
console.log("Hashed Password:", passwordHash);
});
The SaltRounds function is the cost of the hash function. The higher the cost, the more secure hash would be generated. You should decide the salt based on the computing power of your server. Once the hash is generated for a password, the password entered by the user will be compared to the hash stored in the database. Refer to the code below for reference.
const bcrypt = require('bcrypt');
const incomingPassword = "Some-Password@2020";
const existingHash = "some-hash-previously-generated"
bcrypt.compare(
incomingPassword,
existingHash,
(err, res) => {
if(res && res === true) {
return console.log("Valid Password");
}
//invalid password handling here
else {
console.log("Invalid Password");
}
});
Password Storage
Whether you use the database, files to store passwords, you must not store a plain text version. As we studied above, you should generate the hash and store the hash in the system. I generally recommend using varchar(255) data type in case of a password. You can opt for an unlimited length field as well. If you are using bcrypt then you can use varchar(60) field because bcrypt will generate fixed size 60 character hashes.
Authorization
A system with proper user roles and permission prevents malicious users to act outside of their permission. To achieve a proper authorization process, proper roles and permissions are assigned to each user so that they can do certain tasks and nothing more. In Node.js, you can use a famous module called ACL to develop access control lists based on authorization in your system.
const ACL = require('acl2');
const acl = new ACL(new ACL.memoryBackend());
// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// check if the permission is granted
acl.isAllowed('joed', 'blogs', 'view', (err, res) => {
if(res){
console.log("User joed is allowed to view blogs");
}
});
Checkout the acl2 documentation for more information and example code.
Bruteforce Attack Prevention
Bruteforce is an attack where a hacker uses software to try different passwords repetitively until access is granted i.e valid password is found. To prevent a Bruteforce attack, one of the simplest ways is to wait it out approach. When someone is trying to login into your system and tried an invalid password more than 3 times, make them wait for 60 seconds or so before trying again. This way the attacker is going to be slow and it's gonna take them forever to crack a password.
Another approach to preventing it is to ban the IP that is generating invalid login requests. Your system allows 3 wrong attempts per IP in 24 hours. If someone tries to do brute-forcing then block the IP for 24 hours. This rate-limiting approach is been used by lots of companies to prevent brute-force attacks. If you are using the Express framework, there is a middleware module to enable rate-limiting in incoming requests. Its called express=brute.
You can check the example code below.
Install the dependency.
npm install express-brute --save
Enable it in your route.
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
const bruteforce = new ExpressBrute(store);
app.post('/auth',
bruteforce.prevent, // error 429 if we hit this route too often
function (req, res, next) {
res.send('Success!');
}
);
//...
The example code is taken from express-brute module documentation.
Secure Transmission using HTTPS
It is 2021 and you must use HTTPS to send your data and traffic over the internet securely. HTTPS is an extension of the HTTP protocol with secure communication support. By using HTTPS, you can make sure that the traffic and your user's data over the internet is encrypted and safe.
I am not going to explain how HTTPS works in the detail here. We are going to focus on the implementation part of it. I highly recommend you to use LetsEncrypt to generate the SSL certificates for all of your domain/subdomain.
It's free and runs a daemon to update SSL certificates every 90 days. You can learn more about LetsEncrypt here. You can opt for a domain-specific certificate or a wildcard certificate if you have multiple subdomains. LetsEncrypt supports both.
You can use LetsEncrypt for both Apache and Nginx based web servers. I highly recommend performing SSL negotiations in the reverse proxy or at the gateway layer because it is a heavy computing operation.
Session Hijacking Prevention
The session is an important part of any dynamic web application. Having a secure session in the application is a must for the users and systems safety. A session is implemented using cookies and it must be kept secure to prevent session hijacking. The following is a list of the attributes that can be set for each cookie and what they mean:
- secure - this attribute tells the browser to only send the cookie if the request is being sent over HTTPS.
- HttpOnly - this attribute is used to help prevent attacks such as cross-site scripting since it does not allow the cookie to be accessed via JavaScript.
- domain - this attribute is used to compare against the domain of the server in which the URL is being requested. If the domain matches or if it is a sub-domain, then the path attribute will be checked next.
- path - in addition to the domain, the URL path that the cookie is valid for can be specified. If the domain and path match, then the cookie will be sent in the request.
- expires - this attribute is used to set persistent cookies since the cookie does not expire until the set date is exceeded
You can use express-session npm module to perform session management in the Express framework.
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { secure: true, path: '/'}
}));
You can learn more about Express session handling here.
Cross Site Request Forgery (CSRF) Attack Prevention
CSRF is an attack where that manipulates a trusted user of a system to execute unwanted malicious actions on a web application. In Node.js, we can use csurf module to mitigate CSRF attack. This module requires either express-session or cookie-parser to be initialized first. You can check out the example code below.
const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const bodyParser = require('body-parser');
// setup route middlewares
const csrfProtection = csrf({ cookie: true });
const parseForm = bodyParser.urlencoded({ extended: false });
// create express app
const app = express();
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser());
app.get('/form', csrfProtection, function(req, res) {
// pass the csrfToken to the view
res.render('send', { csrfToken: req.csrfToken() });
});
app.post('/process', parseForm, csrfProtection, function(req, res) {
res.send('data is being processed');
});
app.listen(3000);
On the web page, you need to create a hidden input type with the value of the CSRF token. For example.
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
Favorite color: <input type="text" name="favoriteColor">
<button type="submit">Submit</button>
</form>
In the case of AJAX requests, you can pass the CSRF token in the header.
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
headers: {
'CSRF-Token': token
}
Denial of Service
Denial of service or DOS is a type of attack where attackers tried to bring down the service or make it inaccessible to users by disrupting the system. The attacker generally flooded the systems with lots of traffic or requests which in turn increases the CPU and memory load leading to a system crash. To mitigate DOS attacks in your Node.js application, the first step would be the identification of such an event. I highly recommend these two modules to be integrated into the system.
- Account lockout - After n number of failed attempts, lock the account or IP address for a period of time (say 24h?)
- Rate limiting - Limit the users to request the system n number of times within a specific period, for example, 3 requests per minute from an individual user
The Regular expression Denial of service attack (ReDOS)is a type of DOS attack where the attacker exploits the regular expression implementation in the system. Some regular expression takes heavy computing power to execute and the attacker can exploit it by submitting requests that involve regular expression in the system which in turns increases the load on the system leading to system failure. You can use software like this to detect the dangerous regular expressions and avoid using them in your system.
Dependencies Validation
We all use tons of dependencies in our projects. We need to check and validate these dependencies as well to ensure the security of the overall project. NPM already has an audit feature to find the vulnerability of the project. Just run the command shown below in your source code directory.
npm audit
To fix the vulnerability, you can run this command.
npm audit fix
You can also run the dry run to check the fix before applying it to your project.
npm audit fix --dry-run --json
HTTP Security Headers
HTTP provides several security headers that can prevent commonly known attacks. If you are using the Express framework then you can use a module called helmet to enable all security headers with a single line of code.
npm install helmet --save
Here is how to use it.
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet());
//...
This enables the following HTTP headers.
- Strict-Transport-Security
- X-frame-Options
- X-XSS-Protection
- X-Content-Type-Protection
- Content-Security-Policy
- Cache-Control
- Expect-CT
- Disable X-Powered-By
These headers prevent malicious users from various types of attacks such as clickjacking, cross-site scripting, etc.
Author's blog: https://shaikhshahid.com
Top comments (45)
A lot of this is very good advice. However I would add a couple of things:
SameSite=Strict
and you really should be setting this to be true for all cookies. (This was missed when talking about cookies, and it is really easy to do.)bcrypt
set up that was suggested here is now no longer the best practice, it's argon2, although at some point that will also be not the best solution either.I would never consider it a good practice to force anyone to use social sites if they want to use a webapp.
As an optional alternative, sure (personally I wouldn't offer FB login at all and I would never use sites that need it, but that's just me) but not as a full replacement.
Oh, so you want to browse my blog / shop in my store but first you must hand over your data to Facebook / Google. Because security.
Totally agreed. A password-based auth might be old but still works great.
for sure, even if is just one of the steps to log... I really like it yet
That's because most companies are not security competent. Sure I would never hand my data over to Facebook, but Google has proven they don't know what do with it anyway. Realistically you must trust some repository for storing access credentials. Using federated login is much safer than using Lastpass, 1Password, etc... If you want a privacy first federated login, come talk to me in a couple of months, Authress will have that capability.
If you don't use a social login, the site you are using will most likely leak all your data. And sure for a blog you can argue nothing bad, but a site that I put my credit card in, I would never trust that site to handle my data correctly. It usually ends up like this: Hacked home cams used to livestream police raids in swatting attacks.
For credentials, sure. But social login doesn't come with simply credentials storage but with an agreement to allow monitoring online behaviour to the extent that a user needs to put in place a lot of serious safeguards to protect their data and prevent traces, doubly so if they have social media presence.
Without social media profile, some generic data can be leaked, but it's much easier to prevent it even with basic modern browser enhancements.
Password storages, I trust (so far) which is why I prefer the password storages + generated password rather than social login.
I would go password less for 2021, people are just dumb with passwords.
Unless you mean biometrics, I don't see how that goes. Social login means the password is handled by Facebook. But it's still a password just somewhere else, incidentally with a bigger footprint, a bigger target both for hacking and phishing. I have nothing against providing social login as an alternative, just not a replacement for user password (with 2fa, maybe).
If there's a breach on their side, you can't do much except remove that provider.
Totally agreed.
Just email magic links in that case :). Passwords are deprecated.
Absolutely, password are deprecated. That's why nobody uses them anymore and the very few websites that use them are compromised 10 times a day.
Just gonna send an email link to an email address based solely on the existence of that email, which the user is going to access with a ... password. There, the bucket was kicked down the road a bit more.
Points:
"That's why nobody uses them anymore and the very few websites that use them are compromised 10 times a day" - What is your source for these facts, or are they just opinion?
sarcasm
Ok. Funny then but not helpful.
That's what the second part of the post was for. Unless you're looking for validation.
I did a fair bit of reading on CSRF. While we should always try and use
SameSite
attribute for cookies, it is not a fool-proof method for protecting against CSRF. CSRF token should still be used. TheSameSite=Strict
will only work for browsers that implement this feature. If a user uses a browser which does not support it, they can be vulnerable. Additionally there are ways to bypass this feature even for browsers supportingSameSite
attribute.Quoting from CSRF sameSite:
If you are using REST APIs to secure content and not sending it back as part of a SSR. Then those bypasses don't apply. Since it isn't secure to send content back as part of page navigation anyway. CSRF can entirely be replaced by
SameSite
.I would argue that many things you list are controversial.
First, not everybody is building secured services on cloud, and many developpers are building in enterprise private infrastructures.
For many of us:
i appreciate your suggestions but here is my reverse suggestion for webauthn caniuse.com/?search=webauthn check it out, very incompatible on many browsers
Nice work brother. I am making this post more visible.
Also for those who are looking for mongodb security? I have posted a completed write up here: dev.to/tbhaxor/one-step-to-prevent...
Also for express js users, I have created a middleware: mongo-secure.tbhaxor.com/
Great list, and very timely for me. I was asked recently on a call what backend security techniques I am familiar with. On the call I remembered about 1/2 of these, so it’s good to see a full list.
I’ve used all of these apart from perhaps the regex denial of service detection.
Some other libs worth considering:
i have a question here, will highly appreciate the answer by anyone. what is the point of
express-brute
if not been able to use it in production? Thanks.it's for the demonstration purpose. You can use the rate-limiter technique of a similar kind in the production server.
Or maybe having it built. Thanks.
This is a great piece! Thank you. Security still seems to be underemphasized in dev curricula. For many devs, when it comes to learning about security, we don't know where to start. Do you have any pointers/suggested resources on how to get started on cybersecurity?
Start with hacking first (ethical one for sure), learn how to hack a web server, how to scan it for vulnerabilities, look what typical vulnerabilities exist and how to exploit them, it'll give you a lot of knowledge and motivation to create more secure services and websites :). I can send some resources if you're really interested!
Securing application in the best way👍
Great post, fantastic comments. Lots of views!!!
Good job brother, appreciate all the efforts!
Great article. I will definitely consider these in my next project.
Cool Post, thank you very much!
Your welcome :)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.