Node.js Security Guide

Time:2021-10-21

When the project cycle is coming to an end, developers will pay more and more attention to the “security” of applications. A secure application is not a luxury, but a necessity. You should consider application security at every stage of development, such as system architecture, design, coding, including final deployment.

In this tutorial, we will learn step by step how to improve the security of node. JS applications.

1. Data validation – never trust your users

You must verify data from user input or other systems. Otherwise, this will pose a threat to the current system and lead to unimaginable security vulnerabilities. Now, let’s learn how to validate the incoming data in node. JS. You can use the namevalidatorTo perform data validation. For example:

const validator = require('validator');
validator.isEmail('[email protected]'); //=> true
validator.isEmail('bar.com'); //=> false

In addition, you can also usejoiModule to verify the data and model, 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);
  }

2. SQL injection attack

SQL injection allows malicious users to tamper with SQL statements by passing illegal parameters. The following is an example. Suppose you write such an SQL:

UPDATE users
    SET first_name="' + req.body.first_name +  '" WHERE id=1332;

Under normal circumstances, you want this query to be like this:

UPDATE users
    SET first_name = "John" WHERE id = 1332;

But now, if someone willfirst_nameThe value of is passed as follows:

John", last_name="Wick"; --

At this time, your SQL statement will become like this:

UPDATE users
    SET first_name="John", last_name="Wick"; --" WHERE id=1001;

You’ll see,WHEREThe condition has been commented out. This update will update the entire table to all usersfirst_nameChange toJohnlast_nameChange toWick。 Now, you’re in trouble!

How to avoid SQL injection

The most effective way to avoid SQL injection attack is to filter the input data. You can verify each input data one by one or by parameter binding. The most commonly used method by developers is parameter binding, because it is efficient and safe.

If you are using some popular ORM frameworks, such as serialize, hibernate, etc., the framework already provides this data verification and SQL injection protection mechanism.

If you prefer to rely on database modules, such asmysql for Node, you can use the filtering method provided by the database. The following code usesmysql for NodeAn example of:

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) {
    //...
});

??Is replaced by the field name,?The place is replaced by the field value, which ensures the security of the input value.

You can also use stored procedures to improve security, but developers tend to avoid using stored procedures due to the lack of maintainability.

At the same time, you should also perform server-side data validation. However, I do not recommend that you manually verify each field, which can be usedjoiAnd other modules to solve this problem.

Type conversion

JavaScript is a dynamically typed language, that is, values can be of any type. You can use the type conversion method to verify the type of data, so as to ensure that only the data of the specified type can enter the database. For example, the user ID can only be numeric. See the following code:

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) {
    //...
});

Have you noticed the change? Here we useNumber(req.body.ID)Method to ensure that the user ID must be numeric.

3. Application authentication and authorization

Sensitive data (such as passwords) should be stored in the system in a secure way so that malicious users will not abuse sensitive information. In this section, we will learn how to store and manage common passwords. Almost every application has different password storage methods in its system.

Password hash

Hash is a function that generates a fixed size string from an input value. The output value of the hash function cannot be decrypted, so it can be said to be “one-way”. Therefore, for data such as passwords, the value stored in the database must be hash value, not plaintext.

You may want to know, since hash is an irreversible encryption method, how does the attacker obtain password access?

As I mentioned above, hash encryption uses the input string and generates a fixed length output value. Therefore, the attacker takes the opposite approach. They generate a hash from the regular password list, and then compare the hash with the hash in the system to find the password. This attack method is called lookup tables

That’s why you, as a system architect, never allow simple general passwords to be used in the system. To avoid attacks, you can also use something called “salt”, which we call “hash salting”. Salt is attached to the password hash to make the input value unique. The salt value must be random and unpredictable. The hash algorithm we recommend you use isBCrypt, in node.js, you can use the bcyrpt node module to perform hash processing.

Please refer to the code in the following example:

const bcrypt = require('bcrypt');

const saltRounds = 10;
const password = "[email protected]";

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);
});

SaltRoundsFunction is the cost of hash function. The higher the cost, the more secure the hash password generated. You should determine the salt value according to the computing power of the server. After the hash value of the password is generated, the password entered by the user will be compared with the hash value stored in the database. The reference code is as follows:

const bcrypt = require('bcrypt');

const incomingPassword = "[email protected]";
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 a database or a file to store passwords, you cannot use plaintext storage. As we learned in the previous section, you can hash the password and store it in the database. I recommend the password fieldvarchar(255)Data type, you can also choose unlimited length field type. If you usebcrypt, you can usevarchar(60)Field type, because bcrypt generates a hash with a fixed length of 60 characters.

Certification and authorization

A system with appropriate role permissions will prevent some malicious users from doing things beyond their authority in the system. In order to implement the correct authorization process, appropriate roles and permissions are assigned to each user so that they can perform some tasks within their permissions. In node.js, you can use the famous ACL module to develop the access control list according to the authorization in the 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");
    }
});

Consult the acl2 documentation for more information and sample code.

4. Protection against violent attacks

Hackers often use software to repeatedly use different passwords to try to obtain system permissions until they find a valid password. This attack is called violent attack. In order to avoid this attack, a simple and effective way is to “let him wait a minute”, that is, when someone tries to log in to the system and tries to enter an invalid password more than 3 times, please let them wait about 60 seconds before trying again. In this way, attackers will greatly increase the time cost and will never be able to crack the password.

Another way to prevent this attack is to mask the IP of invalid login requests. The system allows three false login attempts per IP within 24 hours. If someone tries to brute force crack, their IP will be blocked for 24 hours. Many companies have used this method to prevent violent attacks. If you use the express framework, there is a middleware module that enables rate limiting in incoming requests. It is calledexpress = brute

Here is an example.

Install dependencies

npm install express-brute --save

Enable it in routing

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!');
    }
);
//...

5. HTTPS secure transmission

Now it’s 2021, and you should also use HTTPS to send data to the network. HTTPS is an extension of the HTTP protocol with secure communication support. Using HTTPS can ensure that the data sent by users in the Internet is encrypted and secure.

I’m not going to introduce the working principle of HTTPS protocol in detail here. We only discuss how to use it. I strongly recommend it hereLetsEncryptTo generate security certificates for all your domain names.

You can use letsencrypt on Apache and nginx based web servers. I strongly recommend that you use HTTPS on the reverse proxy or gateway layer because they have a lot of heavy computing operations.

6. Session hijacking protection

Session is the most important part of any dynamic web application. A secure session is really necessary for users and systems. The session is implemented using cookies, so its security must be ensured to prevent session hijacking. The following is a list of properties that can be set for each cookie and their meaning:

  • secure-This property tells the browser to send cookies only when sending requests over HTTPS.
  • HttpOnly-This attribute is used to prevent cross site scripting attacks because it does not allow cookies to be accessed through JavaScript.
  • domain-This attribute is used to compare with the domain name of the server requesting the URL. If the domain name matches or is its sub domain, the path attribute will be checked next.
  • path-In addition to domian, you can also specify a valid URL path for the cookie. If the domain and path match, a cookie can be sent in the request.
  • expires-This attribute is used to set persistent cookies, which will expire only after the set date.

In the express framework, you can useexpress-sessionNPM module to manage sessions.

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: '/'}
}));

7. Cross Site Request Forgery Attack (CSRF) protection

Cross site request forgery attacks use trusted users in the system to perform harmful and malicious operations on Web applications. In node.js, we can usecsurfModule to mitigate CSRF attacks. The module needs to be initialized firstexpress-sessionorcookie-parser, you can see the following example code:

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 field and save the CSRF token in the input field, for example:

Favorite color: 
  Submit

If an Ajax request is used, the CSRF token can be passed through the request header.

var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  headers: {
    'CSRF-Token': token
  }

8. Denial of service

Denial of service or DOS attack can allow an attacker to destroy the system, force the system to shut down the service or make the user unable to access the service. Attackers usually send a lot of traffic and requests to the system, which increases the server CPU and memory load and leads to system crash. In order to mitigate DoS attacks in node.js applications, the first thing is to identify such events. I strongly recommend integrating these two modules into the system.

  • Account lockout-After n attempts fail, lock the account or IP address for a period of time (for example, 24 hours?)
  • Rate limiting-Limit the user to request the system n times in a specific time period. For example, a single user can only request 3 times per minute.

Regular expression denial of service attack (redos) is a kind of DoS attack. The attacker uses the design defects or computational complexity of regular expressions in the system to consume a large amount of system resources of the server, resulting in service interruption or stop of the server.

We can use some tools to check risky regular expressions to avoid the use of these regular expressions. For example, this tool:

https://github.com/davisjam/vuln-regex-detector

9. Dependency verification

We all use a lot of dependencies in our projects. We also need to check and verify these dependencies to ensure the security of the whole project. NPM already has such an audit function to find vulnerabilities in the project. Just run the following command in the source directory:

npm audit

To fix the vulnerability, run this command:

npm audit fix

You can also do it firstdry runTo check the fix before applying it to the project.

npm audit fix --dry-run --json

10. HTTP security header information

HTTP provides some security header information to prevent common attacks. If you are using the express framework, you can usehelmetModule, one line of code can enable all security headers.

npm install helmet --save

Here’s how to use:

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 HTTP headers can prevent various attacks by malicious users, such as click hijacking, cross site scripting attacks, etc.

(end of this article)

Official account – front-end new world

Recommended Today

Swift advanced (XV) extension

The extension in swift is somewhat similar to the category in OC Extension can beenumeration、structural morphology、class、agreementAdd new features□ you can add methods, calculation attributes, subscripts, (convenient) initializers, nested types, protocols, etc What extensions can’t do:□ original functions cannot be overwritten□ you cannot add storage attributes or add attribute observers to existing attributes□ cannot add parent […]