This article is all about how we can improve the security system of our nodejs application from being attacked or hacked. But first, we need to know what node.js means.Node.js is extremely popular nowadays, primarily as a backend server for web applications. However, in the world of micro-services, you can find it pretty much everywhere, playing different and important roles in a bigger application stack. One of the advantages of Node.js is the ability to install additional modules, which from a security point of view, provides more opportunities to open back doors.
Additionally, the more popular the framework, the more chances that hackers will try to find vulnerabilities. Therefore, you should always take Node.js security seriously. A Developer is tend to consider at the end of the development cycle 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 deployment. As all been said what are those Vulnerabilities or loopholes the hacker tends to find? we will talk about them and the solution as well...
Common attackers on node js application :
- SQL Injection;
- Cross-site scripting (XSS);
- Brute force.
SQL Injection: An SQL injection attack is where an SQL query is ’injected‘ into your web app’s database.
Cross-site scripting(XSS): Cross-site scripting is another type of injection attack, which occurs when an attacker inputs malicious code into a legitimate website by exploiting user inputs.
Brute force: the simplest form of attack, this one involves trying every possibility to find a solution. In your web app, this could mean finding a password by checking millions of likely passwords.
These are the common attack and why we need to secure our node.js applications; below are the solutions to the attack:
Validate user input to limit SQL injections and XSS attacks
Let’s start with one of the most popular attacks, SQL Injection. As the name suggests, a SQL injection attack happens when a hacker can execute SQL statements on your database. This becomes possible when you don’t sanitize the input from the front end. In other words, if your Node.js backend takes the parameter from the user-provided data and uses it directly as a part of the SQL statement.
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 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: "Victor",
email: "abc.com",
password: "123456",
}
const result = schema.validate(dataToValidate);
if (result.error) {
throw result.error.details[0].message;
}
} catch (e) {
console.log(e);
}
To prevent SQL Injection attacks to sanitize input data. You can either validate every single input or validate using parameter binding. Parameter binding is mostly used by 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 or Mongoose, 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) {
//...
});
These as well go for Cross-Site Scripting (XSS) but the difference is that instead of sending malicious SQL, the attacker can execute javascript code.
node
app.get('/find_product', (req, res) => {
...
if (products.length === 0) {
return res.send('<p>No products found for "' + req.query.product + '"</p>');
}
...
});
As you can see in the snippet above, whatever the user puts in the search field, if not found in the database, will be sent back to the user in an unchanged form. What that means is that if an attacker puts JavaScript code instead of the product name in your search bar, the same JavaScript code will be executed. To validate the User input!You can use validator js or xss-filters for that.
Application Authentication and Authorization
Having a broken, weak, or incomplete authentication mechanism is ranked as the second most common vulnerability. It’s probably because many developers think about authentication as “we have it, so we’re secure.” In reality, weak or inconsistent authentication is easy to bypass. Sensitive data such as passwords should be securely stored in the system so that malicious users don't misuse sensitive information.One solution is to use existing authentication solutions like Okta or OAuth.
If you prefer to stick with native Node.js authentication solutions, you need to remember a few things. When creating passwords, don’t use the Node.js built-in crypto library; use Bcrypt or Scrypt.
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 log or print it
console.log("Hashed Password:", passwordHash);
});
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");
}
});
Make sure to limit failed login attempts, and don’t tell the user if it’s the username or password that is incorrect. Instead, return a generic “incorrect credentials” error. You also need proper session management policies. And be sure to implement 2FA authentication. If done properly, it can increase the security of your application drastically. You can do it with modules like node-2fa or speakeasy.
Avoid errors that reveal too much
Next on the list is error handling. There are a few things to consider here. First, don’t let the user know the details, i.e., don’t return the full error object to the client. It can contain information that you don’t want to expose, such as paths, another library in use, or perhaps even secrets. Second, wrap routes with the catch clause and don’t let Node.js crash when the error was triggered from a request.
This prevents attackers from finding malicious requests that will crash your application and sending them over and over again, making your application crash constantly. Speaking of flooding your Node.js app with malicious requests, don’t directly expose your Node.js app to the Internet. Use some component in front of it, such as a load balancer, a cloud firewall or gateway, or old good Nginx. This will allow you to rate limit DoS attacks one step before they hit your Node.js app.
HTTP Security Headers
HTTP provides several security headers that can prevent commonly known attacks. If you are using the Express framework, you can use a module called helmet to enable all security headers with a single line of code.
npm install helmet --save
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.
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
Set up logging and monitoring
You may think that logging and monitoring, while important, aren’t related to security, but that isn’t true. Of course, the goal is to make systems secure from the beginning, but in reality, it requires an ongoing process. And for that, you need logging and monitoring. Some hackers may be interested in making your application unavailable, which you can find out without logging in. But some hackers will prefer to remain undetected for a longer period. For such cases, monitoring logs and metrics will help you spot that something is wrong. With only basic logging, you won’t get enough information to understand if weird-looking requests are coming from your application, a third-party API, or a hacker.
Use security linters
We talked about automatic vulnerability scanning before, but you can go one step further and catch common security vulnerabilities even while writing the code. How? By using linter plugins like eslint-plugin-security. A security linter will notify you every time you use unsafe code practices (for example using eval or non-literal regex expressions).
Avoid secrets in config files
Writing secure code from the beginning will help, but it won’t make your application bulletproof if you end up storing plain text secrets in your config files. This practice is unacceptable even if you store the code in a private repository. Importing secrets from environment variables is the first step, but it’s not a perfect solution either. To be more confident that your secrets aren’t easily readable, use secret management solutions like Vault. Whenever using Vault isn’t possible, encrypt your secrets when you store them and be sure to rotate them regularly. Many CI/CD solutions allow you to securely store secrets and securely deploy them.
I hope you find these node.js application security practices helpful.
Conclusion
Node.js is the master key solution for start-ups to hastily simplify development and it is a sophisticated ecosystem adopted by leading enterprises. Even the safest of all treasures need to be precautious of theft and attacks. And when you are using the most precious web framework for your valuable project, you sure want to shield it from burglary. Therefore we need to be security conscious about nodejs applications.
Top comments (1)
Nice article man