10 Node.js Security Practices
Using the top 10 OWASP security practices as a guideline, this article outlines specific examples of how each can be applied to Node.js.
Join the DZone community and get the full member experience.
Join For FreeWeb application security is rapidly becoming a major concern for companies as security breaches are becoming expensive by the day. The Open Web Application Security Project (OWASP) is a non-profit organization dedicated to web security. OWASP has put together a regularly updated list of the top ten web application security risks.
In the course of this article, we will examine the ten secure practices in Node.js which are in line with the OWASP top 10 web application security risks.
- Use parameterized inputs to prevent injection attacks.
- Use multi-factor authentication to prevent automated attacks.
- Discard sensitive data after use.
- Patch old XML processors.
- Enforce access control on every request.
- Create fluid build pipelines for security patches.
- Sanitize all incoming inputs.
- Scan application for vulnerabilities regularly.
- Secure deserialization.
- Sufficient logging and monitoring.
Use Parameterised Inputs to Prevent Injection Attacks
SQL\NoSQL injection attacks are one of the common attacks on the web today. Attackers often send in queries in the guise of user inputs which forces the system under attack to involuntarily give up sensitive data/information.
A sample scenario for an injection attack can be seen in the snippet below:
const db = require('./db');
app.get('/users', (req, res) => {
db.query('SELECT * FROM Users WHERE UserID = ' + req.query.id);
.then((users) => {
res.send(users);
})
});
The snippet above is susceptible to an injection attack because user input is not sanitised. If the attacker passes in 200 OR 1=1
as req.query.id
, The system will return all the rows from the Users table because of the RHS (right-hand side) of the OR command which evaluates as TRUE.
A secure way of preventing injection attacks is by sanitising/validating inputs coming from the user. Writing a validation rule that only accepts a value of type int will help prevent an injection attack.
Using prepared statements or parameterised inputs can also help to prevent injection attacks because then inputs are treated as inputs and not part of an SQL statement to be executed.
Although the MySQL for Node package doesn't currently support parametrised inputs, Injection attacks can be prevented by escaping user inputs like so:
xxxxxxxxxx
let sql_query = 'SELECT * FROM users WHERE id = ' + connection.escape(req.query.id);
connection.query(sql_query, function (error, results, fields) {
if (error) throw error;
// ...
});
Use Multi-Factor Authentication to Prevent Automated Attacks
Lack of authentication or broken authentication leaves a system vulnerable on many fronts which is why broken authentication is ranked as number two on the top 10 vulnerability list. Vulnerabilities due to broken authentication come as a result of weak password/session management policies implemented in applications.
There are many things to consider when implementing authentication flows such as password (creation and recovery) policies, session ID management, retry policies, etc. In many cases, it is often much easier to leverage already existing solutions such as Okta, Firebase Auth, OAuth, etc.
Aside from leveraging authentication service providers, two-factor authentication (2FA) can be implemented in-app using speakeasy. Speakeasy is an npm package which helps in implementing 2FA for node application by generating one-time tokens.
xxxxxxxxxx
var speakeasy = require("speakeasy");
// generate ascii, hex, base32, otpauth_url
var secret = speakeasy.generateSecret({length: 20});
// generate 6 digit code based on base32 secret
var token = speakeasy.totp({
secret: secret.base32,
encoding: 'base32'
});
// verify token coming from client, will return True if tokens match
var tokenValidates = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: req.query.token,
window: 6
});
// Calculate time step difference in seconds
var tokenDelta = speakeasy.totp.verifyDelta({
secret: secret.base32,
encoding: 'base32',
token: req.query.token
});
The snippet above shows how to get started with 2FA using speakeasy. To get started with multi-factor authentication, you can check out Okta's developer’s blog.
Discard Sensitive Data After Use
Sensitive data exposure occurs when an application exposes personal data belonging to its users or members of staff unintentionally. According to OWASP, sensitive data exposure has been the vulnerability with the common impact. Attackers can steal secrets, hijack user sessions, steal user data as well as perform man-in-the-middle attacks.
Passwords, credit card data, health records and personal information which falls under privacy laws (e.g. GDPR) require extra protection and should be treated with care. Storing sensitive unnecessarily is highly discouraged as data not stored can't be stolen.
To avoid sensitive data exposure, making sure passwords are being encrypted/salted with strong hashing functions such as Argon2, Scrypt, Bcrypt e.t.c is advised.
Also, enforcing HTTP strict transport security (HSTS) on TLS will prevent packet sniffing and man-in-the-middle attacks by allowing access to your app on HTTPS only. This will ensure user data sent from client-side to server-side and back is passed through secure and encrypted channels.
Shown in the snippet below is how to configure HSTS for node applications:
xxxxxxxxxx
// npm install hsts
const hsts = require('hsts')
const sixtyDaysInSeconds = 5184000
app.use(hsts({
maxAge: sixtyDaysInSeconds,
includeSubDomains: false
}))
Patch Old XML Processors
XML external entities attack is an attack in which XML processors are tricked into allowing data from external or local resources. Older XML processors by default allow the specification of an external entity (i.e. a URI which is evaluated during XML processing).
A successful XXE injection attack can seriously compromise the application which it's performed on and its underlying server. An example of an XXE attack can be seen below:
xxxxxxxxxx
<?xml version="1.0" encoding="ISO-8859-1"?>
<email>johndoe@domain.com</email>
</xml>
The snippet above is the XML data containing a users email expected to be processed by the application.
xxxxxxxxxx
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY bar SYSTEM "file:///etc/passwd" >]>
<email>johndoe@domain.com</email>
<foo>&bar;</foo>
</xml>
The snippet above is a malicious xml file which has been tweaked by an attacker to fetch the /etc/passwd file located on the server hosting the application under attack.
Disallowing document type definitions (DTD) is a strong defence against XXE injections attacks.
This can be carried out on the libxmljs XML parser like so:
xxxxxxxxxx
//npm install libxmljs
var libxml = require("libxmljs");
var parserOptions = {
noblanks: true,
noent: false,
nocdata: true
};
try {
var doc = libxml.parseXmlString(data, parserOptions);
} catch (e) {
return Promise.reject('Xml parsing error');
Enforce Access Control on Every Request
Broken access is one of the core skills of attackers given to them by loosely implemented access control policies on the application under attack. Broken access control is usually as a result of insufficient functional testing by application developers.
One way to stay ahead of this vulnerability is by manually testing application modules which require specific user permissions.
Access control rules and middlewares are best implemented on the server-side as it eliminates the possibility of manipulating access permissions from the client-side by cookies or JWT (JSON Web Token) authorisation tokens.
API rate limiting and log access controlling should be set up. This way admins are alerted when there are repeated failures and necessary steps can be taken to mitigate the attack.
The snippet below shows two URLs:
xxxxxxxxxx
http://domain.com/app/profile
http://domain.com/app/admin/profile
A broken access vulnerability can be seen if both URLs can be accessed without any authentication middleware guarding them. If a regular signed-in user without admin privileges can visit the admin page, that in itself is another vulnerability.
Create Fluid Build Pipelines for Security Patches
Security misconfiguration vulnerabilities happen when web applications or servers are left unguarded or guarded with weak security rules. This vulnerability leaves many parts of the application stack (application server, database, containers, etc.) susceptible to malicious exploits.
Soft build pipelines are a major entry point of security misconfiguration typed attacks as development/staging area credentials at times make it to production. This leaves the application vulnerable as development/staging are configurations to have loose security policies.
As such, it's advised to keep all environments (development, staging and production) identical with different credentials and distinct access levels.
Default user account passwords and default package settings also open up vulnerabilities in node applications as attackers can launch brute-force dictionary attacks against login forms using weak credentials.
Default package settings, on the other hand, leave vulnerability breadcrumbs for malicious attackers tail.
An example can be seen in the snippet below:
xxxxxxxxxx
/**
* This will disable the X-Powered-By header
* and help prevent attackers from launching targeted attacks for apps running express
**/
app.disable('x-powered-by');
// Helmet disables X-Powered-By header and also sets other secure HTTP headers
var helmet = require('helmet');
app.use(helmet());
Sanitize All Incoming Inputs
Cross-Site Scripting (XSS) attack is one in which attackers execute malicious JavaScript code on client-side facing applications. This type of attack is of two types, client XSS and server XSS. For this article, we will focus on server XSS.
Server XSS occurs when untrusted input coming from the client-side is accepted by the backend for processing/storage without proper validation. Such data, when used to compute the response to be sent back to the user, may contain executable malicious javascript code.
An example of a server XSS attack can be seen below:
xxxxxxxxxx
..
app.get('/search', (req, res) => {
const results = db.search(req.query.product);
if (results.length === 0) {
return res.send('<p>No results found for "' + req.query.product + '"</p>');
}
});
In the snippet above, a malicious user can easily inject the following code below as req.query.product:
xxxxxxxxxx
<script>alert(XSS in progress!!)</script>
This code will be executed and returned to the user as a failed execution as seen in line 7 above. The problem with this is, a malicious code is returned which capable of causing a lot of harm to the said application/system under attack.
To mitigate XSS attacks, application developers are advised to treat all incoming data from the user as unsafe and as such validate user inputs.
XSS filters can also prevent XSS attacks by filtering data send back the user like so:
xxxxxxxxxx
// npm install xss-filters --save
var xssFilters = require('xss-filters');
app.get('/search', (req, res) => {
const results = db.search(req.query.product);
if (results.length === 0) {
return res.send('<p>No results found for "' +
xssFilters.inHTMLData(req.query.product) +
'"</p>');
}
});
Regularly Scan Applications for Vulnerabilities
The JavaScript eco-system is swarming with open-source packages which make a huge part of the internet today. Keeping track with the versioning and licensing of these open-source packages is often difficult and this breeds outlets for attackers to sneak in malicious code into our applications.
We can keep track of open-source vulnerabilities in our node projects by using the following:
- Package manager provided solutions
- WhiteSource Bolt
Package Manager Provided Solutions
Package managers such as NPM and Yarn helps application developers to block these outlets by locking versions of packages installed to prevent unwanted package updates.
This in itself can cause more harm than good in the sense that outdated packages may contain vulnerabilities and since the package-lock.json
file prevents unsolicited package updates, you'll miss out on install patches which fix these vulnerabilities.
However, by running the $ npm audit
in our project directory, npm can provide us with an audit report for all installed dependencies.
To fix vulnerabilities from running an audit, you can run $ npm audit
fix. This command, however, can't fix all vulnerabilities and will sometimes require manual updates to be done.
Secure deserialization
Insecure deserialization is a flaw that involves deserialization and execution of malicious objects via API calls or remote code execution.
This type of attack can occur in two ways:
- An object/data structure attack which involves the attacker modifying application by executing remote code on application classes which change behavior during serialization.
- This is when legitimate data objects (e.g cookies) are tempered by the attacker for malicious intent.
In order to mitigate/prevent such attacks we will have to prevent cross-site request forgery (CSRF). This can be done by generating a CSRF token from our server and adding it to a hidden field form field.
When said form is submitted, the CSRF middleware checks if incoming token coming matches what was sent before. The middleware will reject request where tokens don't match and accept those that do. This will curb remote code execution as well as legitimate object tempering.
Below is a snippet on how to mitigate cross-site request forgery using CSURF:
xxxxxxxxxx
var cookieParser = require('cookie-parser') // npm install cookie-parser
var csrf = require('csurf') // npm install csurf
var bodyParser = require('body-parser')
var express = require('express')
// setup route middlewares
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })
// create express app
var app = express()
// parse cookies
// 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')
})
// views/forms.html
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{ csrftoken }}">
</form>
Sufficient Logging and Monitoring
The exploitation of insufficient logging and monitoring is the basis for every vulnerability. Attackers often poke systems with all the tools available in their arsenal before they find a point of entry.
Insufficient monitoring often aids attackers because most breaches are only discovered after its occurrence. Most breach studies show it takes over 200 days before a breach is discovered. Many times breaches are not discovered internally, they're discovered by external parties.
Configuring applications and service to a centralized logging system such as Logz will provide an audit trail for ongoing attacks or attempted data breaches. Monitoring tools such as Prometheus enables engineering teams to collect metrics from target systems as well as trigger alert when pre-defined conditions are met.
Regular pen test exercises are encouraged as it will help harden system logging and monitoring policies as well help teams establish response and recovery plans in the event of a breach.
Conclusion
In the course of this article, we have looked at ten secure practices in Node which aligns with OWASP top 10 web application risks.
As frameworks and libraries enable engineers and application developers to build complex and robust systems, they also open up those systems to a ton vulnerability. Staying on top of modern security practices will help application developers build better-secured systems for users.
Further Reads
Opinions expressed by DZone contributors are their own.
Comments