Home/Blog/Programming/Best practices for securing your Node.js apps
Home/Blog/Programming/Best practices for securing your Node.js apps

Best practices for securing your Node.js apps

Maryam Sulemani
Nov 20, 2020
10 min read
content
Popular attacks
Cross-Site Scripting (XSS)
Cross-Site Forgery Requests (CSFR)
Brute force attacks
Npm and potential security risks
Denial-of-Service (DoS) Attacks
SQL injections
How to guard against SQL injections
SQL injection 1=1
SQL injection “=”
SQL injection and batched SQL statements
Best practices for guarding against SQL injections
Add or Remove HTTP Headers
Keep the learning going.
Password encryption in Node.js
Passwords and hashing
Safe defaults and dynamic typing
Protect against race conditions
Callbacks
What to learn next
Continue reading about Node.js and security
share

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Node.js is a JavaScript runtime environment, meaning it has everything you need to execute a program written in JavaScript. Though quite secure by itself, you should still think of additional ways to secure your Node.js apps.

Vulnerabilities are increasing as attackers find new, clever ways to break in to your apps. It’s important to protect your customers’ sensitive data.

In this article, we will look at some popular attacks and threats to Node.js apps followed by steps you can take before the development process to ensure the security of your Node.js app.

Here’s what we will be covering today:


Learn how to secure your Node.js apps hands-on

Consider this course your guide for securing Node.js apps. You’ll learn everything from sanitizing user inputs, encryption, hashing algorithms, and beyond.

Securing Node.js Apps


It’s important to know how hackers/attackers try to breach your Node.js app before you can start defending it. This section will briefly cover some of the popular attacks and threats, and explain how they work.


Cross-Site Scripting (XSS)

Cross-site scripting lets hackers inject malicious client-side scripts into a viewed page, leading to data leaks. They can also redirect users to malicious websites. XSS attacks occur on input forms when they are not properly encoded or validated.


Cross-Site Forgery Requests (CSFR)

CSRF attacks target changes in an application’s state requests leading to end users executing unnecessary actions on authenticated web applications. Hackers can send users links through chat or email, asking them to change email addresses or transfer funds.


Brute force attacks

Brute force attacks involve an attacker relying on automated software that can guess data over time. Brute force attacks can crack encrypted passwords and PINs. Default cookies session name Your actions on a website are stored as cookies. Using default cookie names is a threat, as hackers can easily identify them.


Npm and potential security risks

One of the largest open-source package ecosystems, npm is Node.js’s default package manager. Though npm helps with app performance and developer productivity, you need to keep in mind the dependencies and security risks involved.


Denial-of-Service (DoS) Attacks

DoS attacks either slow down your service or crash it completely. Attackers accomplish this by constantly creating traffic and sending requests.


SQL injections

A common hacking technique that could destroy your database, SQL injections occur when you ask users for input, e.g., their id or username, and they enter an SQL statement instead. Attackers can bypass authentication, retrieve, add, or modify your database.


How to guard against SQL injections

SQL injections are fairly common strategies for attackers, so it’s important to learn how to defend against them. Let’s start with an example.

textuserID = getRequestString("userID");
textSQL = "SELECT * FROM Users WHERE userID = " + textuserID;

In this example, we have a variable (textuserID) that is fetched from the user input. The next line of code selects a user based on their id. Let’s look at some of the ways things can go wrong.


SQL injection 1=1

If your user enters something like 100 OR 1=1 for their userID, the SQL statement looks like this:

SELECT * FROM Users WHERE userID = 100 OR 1=1;

Since 1=1 is always true, the above statement will return all the rows of the Users table. This can be pretty dangerous if your table contains information like usernames and passwords.


SQL injection “=”

Let’s look at another example:

userName = getRequestString("username");

userPass = getRequestString("userpwd");

sql = 'SELECT * FROM Users WHERE Name ="' + userName + '" AND Pass ="' + userPass + '"'

The above code gets the username and password of a user and selects data based on that. If your user enters:

  • Username: ’or’’=’
  • Password: ’or’’=’

The SQL statement becomes:

SELECT * FROM Users WHERE Name ="" or ""="" AND Pass ="" or ""=""

This will return all the rows from the Users table because OR “”=”” is always true.

Enjoying the article? Scroll down to sign up for our free, bi-monthly newsletter.


SQL injection and batched SQL statements

Batched SQL statements are basically multiple SQL statements separated by semicolons. Let’s look at how this works.

SELECT * FROM Users; DROP TABLE Orders

The above example returns all the rows from the Users table and then deletes the Orders table. If your user enters something like 100; DROP TABLE Orders; for their userID, the SQL statement becomes:

SELECT * FROM Users WHERE userID = 105; DROP TABLE Orders;

Best practices for guarding against SQL injections

  • Parameterized Statements: Parameterized statements ensure that your inputs are handled safely. They allow your database to distinguish between data and code, regardless of the input.

If your user enters 100 OR 1=1 as their userID, the parameterized statement will look for a user ID= 100 OR 1=1.

  • Apply input validations: Input validations add an extra layer of protection. You can compare an input to a list of allowed options when writing your validation logic. Input validation also protects you from XSS attacks, DoS, and other injection attacks.

  • Use database accounts with least privilege: Using database accounts with minimum required privileges protects your app from SQL injections. Don’t use admin level rights database accounts in your Node.js app. Create different users with read-write and read-only permissions. Lastly, use SQL views to limit access to specific table columns or joins

  • Sanitize input: You need to ensure you get rid of suspicious-looking input. You can do that by checking that fields like email addresses match a regular expression, alphanumeric or numeric fields do not contain special characters, and strip any newline or white space characters.


Add or Remove HTTP Headers

HTTP headers allow both the server and client to send and receive data about the connection to be established, the resource being requested and the returned resource. You might be ignoring HTTP headers because the users don’t see them. That isn’t the best idea, as they can leak sensitive information.

Express apps leak information through the X-Powered-By header. This header lets the browser know the server’s version and vendor used. Hackers can then cross-reference this with publicly disclosed vulnerabilities allowing your app to be targeted easily.

That’s where Helmet.js comes in, a Node.js module for securing HTTP headers. It has 12 Node modules that interfere with Express, and each module provides configuration options for securing different HTTP headers.



Keep the learning going.

Learn how to secure Node.js apps without scrubbing through videos or documentation. Educative’s text-based courses are easy to skim and feature live coding environments, making learning quick and efficient.

Securing Node.js Apps


Password encryption in Node.js

Password encryption allows you to convert your passwords into an unreadable message using an encryption key. The encryption key is a mathematical value that both you and the recipient know.

The recipient uses the encryption key to turn the random text back to a readable message. Encryption is a two-way function. So, if you encrypt something, you need to decrypt it later.

Encryption has the following two types:

  • Symmetric: This uses the same key for encryption and decryption
  • Asymmetric: This uses different keys for encryption and decryption

Encryption helps protect data from unauthorized users. It also helps with data breaches. If your data is encrypted properly, it will remain secure.


Passwords and hashing

Hashing is one way-encryption. It’s different from encryption because it doesn’t have a decryption key. Here’s how it works, suppose your password is Pa55word. Once passed through a hashing algorithm, it will look something like dhkqhuhdhudhuh.

Every time you enter ``Pa55word, it is hashed and if it matches dhkqhuhdhudhuh`, you can sign in.

You can use the library bcryptjs to secure passwords in Node.js. bcryptjs takes the password and salt, which is the number of times it should execute the hashing algorithm.

The example below uses different function calls for generating the hash and salt.

bcrypt.genSalt(saltRounds, function(err, salt) {
    bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
        // Store hashed password in database
    });
});

Or you could auto generate a hash and salt using:

bcrypt.hash(PlainTextPassword, saltRounds, function(err, hash) {
        // Store hashed password in database
});

You can also compare your password to the hashed password stored in the database using the compare method. If the passwords match, compare returns true, if not, false.

bcrypt.compare(PlainTextPassword, hash, function(err, result) {
    // result == true
});

Safe defaults and dynamic typing

A safe default means you define your variables and properties with a default false, empty, or null state. So, when determining your logical flow, your default is a failure.

For example, if you are checking if a password is correct, you go with the positive application flow, if it isn’t then it should go with the non-positive result.

Here is a basic form validation example:

app.post('/signup', function() {

  //the default logic is failure
  var message = 'Invalid Form Data';


  //check for a valid form
  if (form.validate('signup') === true) {

		//process the form
		var message = 'Form Valid';

  }
});

Node.js is a dynamically typed language. Dynamic typed languages check the variable type as the code runs, this means that the variable’s type will change over its lifetime.

Let’s look at this example:

var isActive = 'false';

if (isActive) {
	console.log("we shouldn't be here, yet we are");
}
else {
	console.log("we should be here");
}

You’re trying to figure out if isActive is true or false, but it’s being returned as a string.Though dynamic typing may make development easier for you, you are more likely to run into errors related to variable type, which may at times be difficult to locate.

Be sure to not completely rely on dynamic typing!


Protect against race conditions

A race condition occurs when you have multiple processes accessing a shared resource at the same time and one or more of them want to modify it. This results in an unexpected logical flow.

Here’s an example. Suppose you have a variable, a=5, you have two processes X and Y. X wants to add 6 to a, and Y wants to add 3 only if a is less than 6. Depending on which process executes first, you will have very different results.

Due to its asynchronous nature, race conditions are uncommon in JavaScript. But if they do appear, they can be very difficult to debug, so it’s best to handle them before they happen. Here’s how you can prevent race conditions.


Callbacks

Callbacks allow you to pass one function as a parameter to another function. The function is then invoked when the process completes. Let’s look at this example. Below, we won’t be using any callbacks:

function generateCsrf(req, res, next) {
//safe default!
req.session.csrfToken = null;
var token = null
//generate a new token
var crypto = require('crypto');
crypto.randomBytes(48, function(err, buffer) {
//this will run after the below
//code has been run
token = buffer.toString('hex');
});
//save the token in the session and proceed
req.session.csrfToken = token;
//let's try to print the token for this demo
console.log(token)
next();
};

The token is null by the time the function next() is called as the token didn’t finish generating. To ensure that this doesn’t happen, we wait for token generation to complete first.

function generateCsrf(req, res, next) {
//safe default!
req.session.csrfToken = null;
//generate a new token
var crypto = require('crypto');
crypto.randomBytes(48, function(err, buffer) {
var token = buffer.toString('hex');
//save the token in the session and proceed
req.session.csrfToken = token;
//let's try to print the token for this demo
console.log(token)
next();
});
};

This concept is also important with database writes. You need transactions to only apply certain changes if all your statements are successful.


What to learn next

Congratulations on making it to the end. I hope you are now familiar with some of the practices for securing your Node.js apps. There is still a lot to learn about securing apps, especially considering all the different requirements out there.

Here’s what we recommend for next learning steps:

  • Authentication
  • Access Control
  • Safe File Handling
  • Typecasting
  • Tools for scanning Node.js apps (Acutinex, NodeJsScan, etc.)

To get started with these concepts and practice what we learned today, check out Educative’s course Securing Node.js Apps. This course will teach you all you need to know from sanitizing input to protecting against XSS and CSRF with a hands-on coding environment. By the end, you will know how to secure a Node.js application.

Happy learning!


Continue reading about Node.js and security