Security Cheat Sheets: Node.js
Quite a lot has already been said about the popularity of NodeJS. The increase in the number of applications is obvious – NodeJS is quite easy to learn, it has a huge number of libraries, as well as a dynamically developing ecosystem.
We made recommendations for NodeJS developers based on OWASP Cheat Sheets to help you anticipate security issues when developing applications.
Security recommendations for NodeJS applications can be divided into the following categories:
- Security during application development;
- Server security;
- Platform security;
Application Development Security
Avoid callback hell
Using callback functions (callbacks) is one of NodeJS’s greatest strengths, however, when nesting callbacks, you can easily forget to handle the error in one of the functions. One way to avoid callback hell is to use promises. Even if the module you are using does not support working with promises, you can always use Promise.promisifyAll (). But even using promises, it is worth paying attention to nesting. To completely avoid the callback hell error, stick to a “flat” chain of promises.
Callback hell example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
function func1(name, callback) { setTimeout(function() { // operations }, 500); } function func2(name, callback) { setTimeout(function() { // operations }, 100); } function func3(name, callback) { setTimeout(function() { // operations }, 900); } function func4(name, callback) { setTimeout(function() { // operations }, 3000); }func1("input1", function(err, result1){ if(err){ // error operations } else { //some operations func2("input2", function(err, result2){ if(err){ //error operations } else{ //some operations func3("input3", function(err, result3){ if(err){ //error operations } else{ // some operations func4("input 4", function(err, result4){ if(err){ // error operations } else { // some operations } }); } }); } }); } }); |
The same code using a flat chain of promises:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
function func1(name, callback) { setTimeout(function() { // operations }, 500); } function func2(name, callback) { setTimeout(function() { // operations }, 100); } function func3(name, callback) { setTimeout(function() { // operations }, 900); } function func4(name, callback) { setTimeout(function() { // operations }, 3000); }func1("input1") .then(function (result){ return func2("input2"); }) .then(function (result){ return func3("input3"); }) .then(function (result){ return func4("input4"); }) .catch(function (error) { // error operations }); |
Limit the size of request
Parsing the request body can be quite a resource-intensive operation. If you do not limit the size of the request, attackers will be able to send large enough requests that can fill up all disk space or exhaust all server resources, but at the same time, limiting the request size for all cases may be incorrect, because there are requests, such as downloading a file. Therefore, it is recommended to set limits for different types of content. For example, using the express framework, this can be implemented as follows:
1 2 3 |
app.use(express.urlencoded({ limit: "1kb" })); app.use(express.json({ limit: "1kb" })); app.use(express.multipart({ limit:"10mb" })); |
It should be noted that an attacker can change the type of request content and circumvent restrictions, therefore, it is necessary to check whether the content of the request matches the type of content specified in the request header. If checking the type of content affects performance, you can only check certain types or queries that are larger than a certain size.
Do not block event loop
An important component of the language is the event loop, which just allows you to switch the execution context without waiting for the operation to complete. However, there are blocking operations whose completion NodeJS has to wait before continuing with the code. For example, most synchronous methods are blocking:
1 2 |
const fs = require('fs'); fs.unlinkSync('/file.txt'); |
It is recommended to perform such operations asynchronously:
1 2 3 4 |
const fs = require('fs'); fs.unlink('/file.txt', (err) => { if (err) throw err; }); |
At the same time, do not forget that the code which comes after the asynchronous call will be executed without waiting for the completion of the previous operation.
For example, in the code below, the file will be deleted before it is read, which can lead to a race condition.
1 2 3 4 5 |
const fs = require('fs'); fs.readFile('/file.txt', (err, data) => { // perform actions on file content }); fs.unlinkSync('/file.txt'); |
To avoid this, you can write all operations in a non-blocking function:
1 2 3 4 5 6 7 |
const fs = require('fs'); fs.readFile('/file.txt', (err, data) => { // perform actions on file content fs.unlink('/file.txt', (err) => { if (err) throw err; }); }); |
Check input fields
Checking input fields is an important part of the security of any application. Validation errors can cause your application to become vulnerable immediately to many types of attacks: sql injection, xss, command injection, and others. To simplify form validation, you can use the validator packages, mongo-express-sanitize.
Shield user data
One of the rules, the implementation of which will help you protect yourself from xss attacks, is to shield user data. You can use the escape-html or node-esapi library for this.
Keep logs
In addition to helping to debug errors, logging can be used to respond to incidents. One of the most popular NodeJS logging packages is Winston and Bunyan. The example below shows how to use Winston to output logs to both the console and the file:
1 2 3 4 5 6 7 |
var logger = new (Winston.Logger) ({ transports: [ new (winston.transports.Console)(), new (winston.transports.File)({ filename: 'application.log' }) ], level: 'verbose' }); |
Control the event loop
If your server is in conditions of intensive network traffic, users may experience difficulties with the availability of your service. This is essentially a DoS attack. In this case, you can monitor the response time and, if it exceeds the specified time, send a message – 503 Server Too Busy. The toobusy-js module can help.
An example of using the module:
1 2 3 4 5 6 7 8 9 10 11 |
var toobusy = require('toobusy-js'); var express = require('express'); var app = express(); app.use(function(req, res, next) { if (toobusy()) { // log if you see necessary res.send(503, "Server Too Busy"); } else { next(); } }); |
Take precautions against brute force
Again modules come to the rescue. For example, express-brute or express-bouncer. Usage example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var bouncer = require('express-bouncer'); bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address // give a custom error message bouncer.blocked = function (req, res, next, remaining) { res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds."); }; // route to protect app.post("/login", bouncer.block, function(req, res) { if (LoginFailed){ } else { bouncer.reset( req ); } }); |
Using CAPTCHA is another common brute force countermeasure. A frequently used module to help implement CAPTCHA is svg-captcha.
Use CSRF Tokens
One of the most reliable ways to protect against CSRF attacks is to use a CSRF token. The token must be generated with high entropy, strictly checked and be connected to the user’s session. To ensure the operation of the CSRF token, you can use the csurf module.
Usage example:
1 2 3 4 5 6 7 8 |
var csrf = require('csurf'); csrfProtection = csrf({ cookie: true }); app.get('/form', csrfProtection, function(req, res) { res.render('send', { csrfToken: req.csrfToken() }) }) app.post('/process', parseForm, csrfProtection, function(req, res) { res.send('data is being processed'); }); |
Do not forget to add the token to the hidden field on the page:
1 |
<input type="hidden" name="_csrf" value="{{ csrfToken }}"> |
Delete unnecessary routes
The web application should not contain pages that are not used by users, as this can increase the attack surface. Therefore, all unused API routes must be disabled. You should especially pay attention to this question if you use Sails or Feathers frameworks, since they automatically generate API endpoints.
Protect yourself from HPP (HTTP Parameter Pollution)
By default, express adds all the parameters from the request to an array. OWASP recommends using the hpp module, which ignores all parameter values from req.query and / or req.body and simply selects the last value from among the repeated ones.
1 2 |
var hpp = require('hpp'); app.use(hpp()); |
Monitor return values
For example, the user table can store important data: password, email address, date of birth, etc. Therefore, it is important to return only the necessary data.
For example:
1 |
exports.sanitizeUser = function(user) { return { id: user.id, username: user.username, fullName: user.fullName }; }; |
Use descriptors
Using descriptors, you can describe the behavior of a property for various operations: writable – whether it is possible to change the value of a property, enumerable – whether it is possible to use a property in a for..in loop, configurable – whether it is possible to overwrite a property. It is recommended to pay attention to the listed properties, since when defining the property of an object, all these attributes are set to true by default. You can change the value of properties as follows:
1 2 3 4 5 6 7 |
var o = {}; Object.defineProperty(o, "a", { writable: true, enumerable: true, configurable: true, value: "A" }); |
Use ACL
The acl module can help to differentiate data access based on roles. For example, adding permission looks like this:
1 2 3 4 |
// guest is allowed to view blogs acl.allow('guest', 'blogs', 'view') // allow function accepts arrays as any parameter acl.allow('member', 'blogs', ['edit', 'view', 'delete']) |
Catch uncaughtException
By default, in the case of an uncaught exception, NodeJS will print the current stack trace and terminate the execution thread. However, NodeJS allows you to customize this behavior. In case of an uncaught exception, an uncaughtException event is raised, which can be caught using the process object:
1 2 3 4 5 |
process.on("uncaughtException", function(err) { // clean up allocated resources // log necessary error details to log files process.exit(); // exit the process to avoid unknown state }); |
It is worth remembering that when an uncaughtException occurs, you must clear all allocated resources (for example, file descriptors and handlers) before completing the Z process in order to avoid unforeseen errors. It is strongly discouraged to continue the program if an uncaughtException occurs.
Also, when displaying error messages, the user should not disclose detailed error information, such as stack trace.
Server security
Set flags for headers when working with cookies
There are several flags that can help to protect against attacks such as xss and csrf: httpOnly, which prevents access to cookies through javascript; Secure – allows sending cookies only via HTTPS and SameSite, which determines the ability to transfer cookies to a third-party resource.
Usage example:
1 2 3 4 5 6 |
var session = require('express-session'); app.use(session({ secret: 'your-secret-key', key: 'cookieName', cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true} })); |
Set HTTP Headers for Security
Below are the headers and examples of their connection, which will help you protect yourself from a number of common attacks. Headers are set using the helmet module
• Strict-Transport-Security: HTTP Strict Transport Security (HSTS) tells the browser that the application can only be accessed via HTTPS
1 2 |
app.use(helmet.hsts()); // default configuration app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration |
• X-Frame-Options: determines whether the page can be used in frame, iframe, embed or object
1 2 3 |
app.use(hemlet.xframe()); // default behavior (DENY) helmet.xframe('sameorigin'); // SAMEORIGIN helmet.xframe('allow-from', 'http://alloweduri.com'); //ALLOW-FROM uri |
• X-XSS-Protection: Allows the browser to stop loading the page if it detects an XSS reflected attack
1 2 |
var xssFilter = require('x-xss-protection'); app.use(xssFilter()); |
• X-Content-Type-Options: used to prevent attacks using MIME types
1 |
app.use(helmet.noSniff()); |
• Content-Security-Policy: Prevents attacks such as XSS and data injection attacks.
1 2 3 4 5 6 7 8 9 10 |
const csp = require('helmet-csp') app.use(csp({ directives: { defaultSrc: ["'self'"], // default value for all directives that are absent scriptSrc: ["'self'"], // helps prevent XSS attacks frameAncestors: ["'none'"], // helps prevent Clickjacking attacks imgSrc: ["'self'", "'http://imgexample.com'"], styleSrc: ["'none'"] } })) |
• Cache-Control and Pragma: for managing caching, especially this header can be useful for pages that contain sensitive data. However, remember that disabling caching on all pages can affect performance.
1 |
app.use(helmet.noCache()); |
• X-Download-Options: header prevents Inter Explorer from executing downloaded files
1 |
app.use(helmet.ieNoOpen()); |
• Expect-CT: Certificate Transparency – a mechanism created to solve some problems with the infrastructure of SSL certificates, this header tells the browser about the need for additional certificate verification in CT logs
1 2 3 4 |
var expectCt = require('expect-ct'); app.use(expectCt({ maxAge: 123 })); app.use(expectCt({ enforce: true, maxAge: 123 })); app.use(expectCt({ enforce: true, maxAge: 123, reportUri: 'http://example.com'})); |
• X-Powered-By: An optional header that is used to indicate the technology used on the server. You can hide this header as follows:
1 |
app.use(helmet.hidePoweredBy()); |
In addition, you can change the value to hide real information about the technologies you use:
1 |
app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' })); |
Platform security
Update your packages
The security of your application depends on the security of the packages you use, so it is important to use the latest version of the package. To make sure that the package you are using does not contain known vulnerabilities, you can use the special OWASP list. You can also use the library that checks packages for known vulnerabilities Retire.js.
Do not use unsafe functions
There are functions that are recommended to be discarded whenever possible. Among these functions is eval (), which executes the string taken as an argument. In combination with user input, using this function can lead to a remote code execution vulnerability, since for similar reasons, using child_process.exec is also unsafe, because the function passes the received arguments to bin / sh.
In addition, there are a number of modules that you should use with caution. For example, the fs module for working with files. If in a certain way the generated user input is passed into a function, then your application may become vulnerable to including a local file and directory traversal.
The vm module, which provides an API for compiling and running code on a V8 virtual machine, should be used only in the sandbox.
Be careful using regular expressions
A regular expression can be written so that you can achieve a situation where the runtime of the expression will grow exponentially, which can lead to a denial of service. Such attacks are called ReDoS. There are several tools to check if regular expressions are safe, one of which is vuln-regex-detector.
Run linter periodically
During development, it’s hard to keep all safety recommendations in mind, and when it comes to team development, it’s not easy to achieve compliance with the rules by all team members. For such purposes, there are tools for static security analysis. Such tools, without executing your code, look for vulnerabilities in it. In addition, linter allows you to add custom rules for finding places in the code that may be vulnerable. The most commonly used linters are ESLint and JSHint.
Use strict mode
Javascript has a number of insecure and obsolete features that should not be used. To exclude the possibility of using these functions, strict mode is also provided.
Adhere to general safety principles
The recommendations described focus on NodeJS, but do not forget about the general security principles that must be observed regardless of the platform used.
Related Posts
Leave a Reply Cancel reply
Service
Categories
- DEVELOPMENT (103)
- DEVOPS (53)
- FRAMEWORKS (26)
- IT (25)
- QA (14)
- SECURITY (13)
- SOFTWARE (13)
- UI/UX (6)
- Uncategorized (8)