How to Secure Node.js/Express Apps?
Quick tips that can save your Node app from security attacks
Quick tips that can save your Node app from security attacks
Node.js is one of the most popular runtimes used along with frameworks such as Express. It has a flourishing community of developers and is well-loved by everyone. While developing with Node.js is a joy and it is also important to think of the app you have built from a security standpoint.
John Au-Yeung has written a quick read on this topic as well here on methods such as prevent brute force attacks against authorization, root usage, eval statements and more. In this article, we will discuss some simple yet effective tips to safeguard your Node.js/Express.js apps from attacks which expands further from the article. Together, this can be a wholesome guide on improving security mechanisms in your Node.js apps.
Let’s dive right into it!
Validating Input from Users
Form Validations
One of the key principles when developing any software application is to ensure that the data flowing into your application is always validated as per your conditions. Without validation, malicious input may enter your application.
In Node.js, we have the luxury of leveraging the NPM registry which has a huge number of libraries. For validation purposes, we can use the validator and express-validator libraries.
For example, with the validator package, you can do simple validations to check if a string being input is an email:
const validator = require('validator');
validator.isEmail("abc@xyz.com");
Similarly, it offers a huge range of operations such as isAlpha
and sanitising functions such as escape
or trim
.
Express validator is another library which is a middleware to Express.js. A simple example of validating the fields in the request body of an API call is shown here:
// ...rest of the initial code omitted for simplicity.
const { body, validationResult } = require('express-validator');
app.post(
'/user',
// username must be an email
body('username').isEmail(),
// password must be at least 5 chars long
body('password').isLength({ min: 5 }),
(req, res) => {
// Finds the validation errors in this request and wraps them in an object with handy functions
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
User.create({
username: req.body.username,
password: req.body.password,
}).then(user => res.json(user));
},
);
Refer to this page for detailed documentation.
Preventing SQL Injection
Once again, SQL injection attacks arise when the input from users are not sanitised. This can cause a SQL query submitted in form fields to be executed on the database. Imagine how will an operation such as DROP DATABASE dbName;
will affect your application! It will wipe the data clean.
In this case, the most straightforward approach is also to use a library such as validator (shared above) to thoroughly sanitise input to ensure special characters, weird query strings etc. are removed before going into the database layer in your code.
Avoiding Type Mismatches
Next up, we need to avoid type mismatches in our code. For example, we cannot received a string when our function expects a number. JavaScript is a weakly typed languaged. We will not be required to specify the type of a variable in advance and JavaScript will attempt to “type” it accordingly.
While this can be easier when developing, it may give rise to some security flaws and bugs. To avoid this, we can use native casting functions in JavaScript such as:
Number(“123”)
String(123)
There are many other type conversions you can use. Do check this page for more info.
Accounts Management
Password Hashing
A crucial part of your application is user management. Your app most likely contains user accounts to allow users to sign in. Such functions most likely would use some password for signing in.
Passwords are classified as sensitive information and should be stored in a secure way possible such that it will not be misused in the hands of the wrong person. The basic measure in password management is salting and hashing.
Hashing refers to a one-way function which converts the input to a string of undecodable bytes. However, using hashing solely will not solve the problem. An attacker can easily use lookup tables which contains precomputed hashes of the most popular passwords to compare the hash in your app database (in case of a hack).
This is where salting comes in. By adding a random “salt”, we append a unique string of characters to the plaintext password supplied by the user and then hash it. With this, the probability of decoding a hashed password with lookup table is significantly reduced.
For Node.js or Express.js apps, we can take advantage of the BCrypt library.
const bcrypt = require('bcrypt');
const saltRounds = 10;
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(userPassword, salt, function(err, hash) {
dbHelper.save(hash);
});
});
Above is a simple example of how you can achieve this in just a few lines!
The saltRounds
is the number of rounds of the salt process which is correlated to the security of the hash. More rounds = more secure, however more rounds = more expensive operation.
Authorization
The next part of account management is authorization. Simply put,
Authorization is the function of specifying access rights/privileges to resources, which is related to general information security and computer security, and to access control in particular.
Credits: Wikipedia
Hence, using authorization we can ensure that only the right people/users have access to the right resources.
In Node.js/Express.js, we can achieve this using the ACL library. For example,
// allow function accepts arrays as any parameter
acl.allow("member", "blogs", ["edit", "view", "delete"]);
The above example from the docs show that we allow the “member” to “edit”, “view” and “delete” the resource called “blogs”.
Authorization along with authentication is important. Do not confuse the two, and each is important in their very aspect.
Authentication is the process of ascertaining that somebody really is who they claim to be.
Authorization refers to rules that determine who is allowed to do what. E.g. Adam may be authorized to create and delete databases, while Usama is only authorised to read.
Credits: StackOverflow
Network Security
HTTPS and SSL
Beyond your app, you also need to transmit and receive data in a secure manner to avoid any snooping. Today, every app should support HTTPS as the de-facto standard.
Though not unique to Node.js/Express.js, the simplest way to enforce SSL encryption for your apps is to use LetsEncrypt. This is a free-to-use service that provides free SSL certificates to secure your app domain.
This should work on the web server level, such as with Nginx. Do check out the docs here.
Another way is to of course obtain SSL certificates from your cloud provider, such as Amazon Certificate Manager (ACM) on AWS.
Security Headers
Many attacks can be prevented with just the presence of some key security headers. To implement this, we can use helmet. With just one line of code, you can enable several security headers:
app.use(helmet());
The above translates to:
app.use(helmet.contentSecurityPolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard());
app.use(helmet.hidePoweredBy());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());
Simple, easy, and effective!
Others
CSRF / XSRF Mitigation
Another popular attack you might need to mitigate will be Cross-Site Request Forgery. This attack tricks a legitimate user into submitting malicious content. For example, making a user execute a malicious script when logged in (authenticated state).
This attack can be mitigated using the csurf library. This package requires the use of a session middleware or cookie-parser. Most commonly, it has been used with Express.js as a middleware. Check out the docs here.
This middleware helps create a csrf-token which will be passed along with network calls. This token will then be validated against the user’s session or the cookie. Read more here.
Packages and Dependencies
While the NPM repository is huge and provides an easy and accessible way to augment your apps, we also need to take precautions to be exposed to security risks due to outdated packages.
Hence, regular monitors to ensure all packages are up-to-date is necessary. We can do this easily using the npm audit
command and fix automatically fixable issues with npm audit fix
.
It’s that simple!
Conclusion
Today, the security of apps we develop takes a huge priority in ensuring the stability, reliability, and durability of your services. Protecting user trust is of course another huge responsibility we have as developers. The above tips are some ways in which we can augment our Node apps to run safely and minimise security risks.
Happy coding! 💻
More content at plainenglish.io