Categories
Node.js Best Practices

Node.js Best Practices — Security and Performance

Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.

In this article, we’ll look at some security and performance considerations when building and running Node.js apps.

Take Care When Working with Child Processes

We’ve to be careful with inputs. This means we should sanitize to prevent any shell inject attacks. We don’t want attacks to run commands with a child process instance that we created from our app.

If we don’t take precautions, we can easily let attackers run commands remotely from our system by entering malicious inputs with unsanitized system commands.

Hide Error Details from Clients

Clients shouldn’t know about the parts of our system or any private data. Therefore, we should hide any information that’s exposed to errors.

We should ensure that we don’t return the entire error object to the client, which may contain some sensitive app details. Sensitive things include file paths, 3rd-party modules, internal workflows of an app, etc., which can be exploited by an attacker.

Configure 2FA for NPM or Yarn

We can use multi-factor authentication to access our private package repositories. This way, attacker’s can’t publish packages that are modified by them.

If these attacks go undetected, then our packages may be exposing data or doing something that we don’t want attackers to do to our system.

Modify Session Middleware Settings

We should remove any information that tells an attacker about our system. One of them if which web framework we use to develop our app. This is because attackers can use this information to look up how to attack our systems.

Therefore, we should hide things like the X-Powered-By response header, which returns the framework that’s used by default in Express.

Avoid DOS Attacks by Explicitly Setting When a Process Should Crash

The Node process will crash when errors aren’t handled. Many suggest that the process should exit even if the error is handled.

If we leave our app to crash when entering certain inputs, then attackers can find the pattern of what inputs make our apps crash. This isn’t good since we don’t want them to find ways to crash our system.

Any time our app crashes, a critical alert should be sent so that we’re aware of it.

Prevent Unsafe Redirects

We should validate user input to prevent malicious redirects so that attackers can’t launch phishing scams to steal user credentials.

Avoid Publishing Secrets to the NPM Registry

Secrets should never in the NPM registry. Otherwise, we increase the chance that we expose the secrets to the public. We can use npmignore file to blacklist specific files or folder, or the files array in package.json can act as a whitelist.

Don’t Block the Event Loop

Since Node.js is single-threaded, we shouldn’t run any synchronous code that takes a long time. This way, we won’t block the event loop so that other pieces of code will run.

Whenever we have long-running processes, we should use asynchronous code like promises so that these kinds of code only runs when the app get a result.

Otherwise, one request can hang the app that thousands of users are using.

Prefer Native JS Methods Over 3rd-Party Utilities Like Lodash

If a function is available in the JavaScript standard library, then we should use it. This way, we need fewer dependencies and therefore, our app will run faster.

Native JavaScript functions are now about 50% faster than utility libraries.

Conclusion

We should validate the inputs of child processes to eliminate risks from malicious commands being entered and injected by attackers. Unsafe redirects should be checked so that users won’t be redirected to malicious sites.

Also, we should hide any information that may be useful to attacks like what framework is used, stack traces, etc. Secrets should never be in the NPM registry.

Performance-wise, we should use JavaScript standard libraries as much as possible to increase performance to reduce the number of dependencies used.

Categories
Node.js Best Practices

Node.js Best Practices — Improving Security

Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.

In this article, we’ll look at how to improve the security of our Node.js apps.

Prevent Brute-Force Attacks Against Authorization

We should rate limit our login routes so that attackers can’t have too many failed login attempts in a period of time. To do this, we can check the IP address or the ID or name of the computer that’s trying to log in.

Otherwise, attackers may make enough guesses to make the correct guess to log in.

To add brute force protection to an Express app, we can use the express-brute package. We can use it as follows:

const express = require('express');
const bodyParser = require('body-parser');
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore();
const bruteforce = new ExpressBrute(store);

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.post('/auth',
  bruteforce.prevent,
  (req, res, next) => {
    res.send('Success!');
  }
);

app.listen(3000, () => console.log('server started'));

Then when an attack hits the route too often, then the attacker will get a 429 response indicating the route has been hit too many times from the same origin.

Run Node.js as Non-Root User

Running a Node.js app as a root user is dangerous. It can do anything to the computer, which is probably not what we want. To reduce the risks from running our Node app, we should create a non-root user and bake it into our Docker image.

Then we can add the -u username option to run the Docker container with the specified user. Replace username with the username of your user.

Attackers then can’t run anything on our system.

Limit Payload Size Using a Reverse-Proxy or a Middleware

We can limit the payload size of requests by using reverse proxies or middleware to control request payload sizes. For instance, we can set the limit option of the Express body-parser middleware to limit the payload size as follows:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json({ limit: '200kb' }));
app.use(bodyParser.urlencoded({ extended: true }));

app.post('/', (req, res, next) => {
  res.send('hello');
});

app.listen(3000, () => console.log('server started'));

We just set the limit value to '200kb' to limit the request payload size to 200KB.

Avoid JavaScript Eval Statements

We should never use the eval function in any code. This is because it lets attackers run code that’s injected into eval . Also, the performance from running code from a string is slow.

Another similar construct that should be avoided is the new Function constructor. It takes one or more string arguments for parameter names and code to run. Like eval , it lets people run code from a string, which means people can run anything with strings.

setTimeout and setInterval should also never behave dynamic JavaScript code passed in as arguments.

If we eliminate these constructs, we eliminate lots of risks for cross-site scripting attacks as attackers can’t inject malicious via these constructs.

Prevent RegExp From Overloading Single Thread Execution

Regular expression processing in JavaScript is inefficient. A single request that validates a few words can block the entire event loop for a few seconds.

Therefore, we should use 3rd party validation packages like validator.js instead of writing our own regex patterns. Also, we should make use of safe-regex to detect vulnerable regex patterns.

Avoid Module Loading Using a Variable

Module loading should never be done with variables. This allows attackers to potentially inject malicious code to import malicious code into our app and run it.

This also applies to anything that’s for accessing files like reading files. Eslint-plugin-security can detect this and stop you from committing such changes.

Run Unsafe Code in a Sandbox

Sandbox execution environments are great for testing out code that’s potentially unsafe. We can use something like cluster.fork() to create a new sandboxed environment to run any potentially bad code.

Conclusion

When we run Node apps, we should think about potential security issues when writing apps. Never should we let users run code dynamically. This means we should avoid using constructs like eval , dynamic imports, Function constructor, and other things like that.

To prevent brute-force attacks, we should rate limit our authentication API endpoints to avoid brute-force attacks. In JavaScript, regex can block the event loop for a long time. Therefore, we let others create our regex and validate it by using 3rd-party libraries.

Categories
Node.js Best Practices

Node.js Best Practices — Maintaining Production Code

Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.

In this article, we’ll look at the best practices for maintaining production code.

Discover Errors and Downtime Using APM Products

Application monitor and performance (APM) products check our codebase and API so that it can go beyond traditional monitoring and measure the overall user experience across services and tiers.

Some of these like Scout can check for repeated and slow database queries for example so that we can look at them and have to fix those issues that are listed.

Otherwise, we may spend lots of time measuring API performance and downtime and we may still miss the slowest code parts in production.

Make Our Code Production-Ready

We should plan for production on day 1 so that we don’t have to worry about issues that only arise after deploying to production.

Measure and Guard Memory Usage

Our apps shouldn’t be using too much memory. Also, we should be aware of memory leaks in our apps. In small apps, we may gauge memory periodically using shell commands, but in medium and large apps we can use more robust monitor tools to watch for memory usage.

An APM tool may help with this as well. For Express apps, we can add the express-status-monitor package to watch the status of our app, including CPU and memory usage, response time, request per second, and more.

We just have to install the package by running:

npm i `express-status-monitor`

Then we can use it by writing the following code:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(require('express-status-monitor')());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res, next) => {
  res.send('hello')
});

app.listen(3000, () => console.log('server started'));

We just put the package straight into our app with app.use(require(‘express-status-monitor’)()); .

Then when we go to the /status page, we’ll see all the performance and health metrics listed.

Get Frontend Assets Out of Node

Frontend assets shouldn’t be in our Node app. Instead, we can host them in their own location with services like S3. This way, our front end code isn’t tightly coupled with our Node app, and it’s also faster since our Node app doesn’t have to hosts these assets in addition to running its own code.

If we move them to another server, then we won’t tie up our Node app by serving hundreds of HTML, CSS, JavaScript, and media files.

Be Stateless and Kill Our Servers Almost Every Day

We should store any kind of data with external data stores. This includes sessions, cookies, cache and uploaded files. We can enforce this by killing and rebuild our servers daily.

The reason we want to do this is to make our app independent of its data. Otherwise, failure in the app’s server will result in downtime instead of just killing the faulty server. Scaling out will also be more challenging because of the reliance on a specific server.

Use Tools that Automatically Detect Vulnerabilities

Tools that detect vulnerabilities helps us check for them without doing any work ourselves. This is good because we don’t want to check all our code and external packages manually since there’s so much code.

These tools warn us if it finds any vulnerabilities so we can fix them soon.

Assign a Transaction ID to Each Log Statement

Assigning transaction IDs to log statements helps us identify each log entry easily as it has a unique ID associated with it. For example, we can log each entry with a unique ID with morgan by using it in conjunction with the uuid module and our own middleware.

For instance, we can write the following:

const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan')
const uuid = require('uuid')
const fs = require('fs');
const path = require('path');

morgan.token('id', (req) => {
  return req.id
})

const app = express();
app.use((req, res, next) => {
  req.id = uuid.v4()
  next()
})
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' })
app.use(morgan(':id :method :url :response-time', { stream: accessLogStream }))

app.get('/', (req, res, next) => {
  res.send('hello');
});

app.listen(3000, () => console.log('server started'));

In the code above, we added our own middleware to set req.id to a UUID. Then we write that to the file along with the response time.

Conclusion

We can add monitoring to our app with APM products. To free our app from unnecessary work, we should move our front end assets out of our Node app. Also, Node apps should be stateless. We can enforce that by killing and rebuilding the server every day. It prevents issues with losing data that we need on a server.

ID should be assigned to log entries so that we can look them up later.

Categories
Node.js Best Practices

Node.js Best Practices — Production and Security

Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.

In this article, we’ll look at deployment and security-related best practices for Node apps.

Set NODE_ENV=production

We should set NODE_ENV to production for production environments and development for development. Setting NODE_ENV to production makes production-related optimizations activate.

Omitting this single property may degrade performance significantly. For instance, using Express without NODE_ENV set to production makes it slower by a factor of 3.

In Express, if NODE_ENV is production , it’ll cache view templates and CSS generated from CSS extensions, and it’ll also generate less verbose error messages to keep them from being exposed to the public.

Design Automated, Atomic and Zero-Downtime Deployments

Deployments need to be automated so that we don’t have to worry about it once the automated deployment pipeline is set up. This way, we don’t have to worry about running into issues caused by human error. Manual deployments are also much slower than automatic.

Even better, we can create automated sandbox environments with Docker so that each app runs in its own environment. This solves lots of issues with conflicting runtimes, packages, and environment variables.

Otherwise, we have to deploy manually every time, which may cause issues. Also, we have to watch it to make sure that it’s done, and so people are less likely to deploy apps since it’s such a painful and error-prone process.

Use an LTS Release of Node.js

Long Term Service (LTS) releases of Node are supported for much longer than non-LTS Node versions. They constantly receive critical bug fixes, security updates, and performance improvements. Non-LTS releases are only supported for a few months after its releases with updates.

Therefore, to avoid having to upgrade Node all the time, we should use the LTS versions.

Embrace Linter Security Rules

ESLint plugins like eslint-plugin-security checks for security issues with our code so that we can fix them as early as possible. This helps catch security weaknesses like eval , invoking a child process, or importing a module with user inputted string.

With it, we can check before our code is committed or pushed. With this, we also follow security best practices all the time by everyone in the team.

Limit Concurrent Requests Using a Middleware

Denial of Service (DOS) attacks are very popular and easy to do. Therefore, we should implement rate-limiting using a service like load balances, cloud firewalls, reverse proxy, or a package in our app.

To implement rate-limiting in our app endpoints, we can use packages like rate-limiter-flexible or express-rate-limit .

For instance, we can use express-rate-limit as follows to add a rate limit to all the endpoints in our Express app:

const express = require('express');
const bodyParser = require('body-parser');
const rateLimit = require("express-rate-limit");

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});

app.use(limiter);

app.get('/', (req, res, next) => {
  res.send('hello');
});

app.listen(3000, () => console.log('server started'));
module.exports = app;

As we can see, we only have to add a simple middleware to prevent denial of service attacks on our app. Therefore, we should do it from the beginning for apps of any size.

Extract Secrets from Configuration Files or Use Packages to Encrypt Them

Configuration files often have secret data that shouldn’t be seen by most people. Therefore, we should never store them in our source code. Instead, we have to make sure of secret-management systems like Vault, Docker secrets, or use environment variables.

We should have pre-commit or push hooks to prevent accidentally committing secrets to code. Source control can be mistakenly made public, which exposes the secrets to other people that shouldn’t see them.

For example, we can use the dotenv package to store environment variables for Node apps. To use it, we write:

const express = require('express');
const bodyParser = require('body-parser');
require('dotenv').config();

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res) => {
  res.send(process.env.DB_HOST);
});

app.listen(3000, () => console.log('server started'));

Given that our .env file has:

DB_HOST=foo

Once we ran:

require('dotenv').config();

Then process.env.DB_HOST returns 'foo' , so we’ll see foo displayed on the screen if we go to / .

Conclusion

We should make sure that NODE_ENV is set to production so that optimizations are applied for packages like Express.

Any deployment should be automated. Also, Node.js should be the LTS version. Also, we should check for security vulnerabilities in our code and also rate limit our API endpoints to prevent denial of service attacks.

Finally, we should keep secrets out of our code.

Categories
Node.js Best Practices

Node.js Best Practices — Going to Production

Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.

In this article, we’ll look at what we should with our Node app before and after going to production.

Use Production-Like Environment for E2E Testing

Running end to end tests in a production-like environment ensures that we won’t run into different issues in productions that we catch beforehand.

Also, we should run our tests in a database with clean data so that we can repeat our tests.

Refactor Regularly Using Static Analysis Tools

Before putting our code to production, we should refactor our code so that it runs quickly in production. The automated tests will help us to make sure that refactoring won’t break any existing functionality.

Poor code quality will create more bugs and performance issues that are hard to fix.

Carefully Choose Our CI Platform

Jenkins and CircleCI are popular platforms for continuous integration. Having a CI (continuous integration) pipeline lets us run tests and deploy automatically in the background rather than running everything manually. It frees us to do other kinds of work and frees us from manually managing the infrastructure.

We’ve to choose carefully since migration from one to the other will be a pain.

Monitoring Our App

We should monitor our app so that our app runs properly and not taking up too many resources. To do this, we can use monitoring tools and add health check endpoints to check if our app is running.

This way, we don’t have to let our customers tell us that our app has failed.

Increase Transparency Using Smart Logging

Logging lets us troubleshoot problems easily by spotting the activities in the lo that may be causing problems. Most logging platforms can control how logs are collected, stored, and analyzed to ensure that it’s only storing the data that we want.

Delegate Anything Possible to a Reverse Proxy

If something can be done with a reverse proxy, then they don’t belong in our app. CPU intensive tasks like SSL, Gzipping, termination should all be done on a reverse proxy to take the load off our app.

This is especially important for Node apps since it only runs on one thread, so we don’t want to tie it up by making it do infrastructure-related tasks that belongs to the reverse proxy.

Lock Dependencies

We should lock our app’s dependencies so that they won’t change versions across environments. Nowadays, this should be done automatically since npm install generates a package-lock.json if it doesn’t exist. If it does exist, then npm install will use the versions in the file to install the dependencies.

If it doesn’t exist in our repo or if we use fine-grained control of how the versions are locked, we can run npm shrinkwrap . This command repurposes package-lock.json into a publishable npm-shrinkwrap.json or creates a new one.

It takes precedence over package-lock.json .

Guard Process Uptime Using the Right Tool

Our app must be restarted when it fails. We can use Forever or PM2 to watch our app and restart it when it crashes. If we have a cluster, then we also have to manage that.

Utilize All CPU Cores

We should use all the CPU cores to run our app with the fastest performance possible. A CPU core is useless if it’s left idling. If that’s the case, we should replicate the Node processes and utilize all CPUs. For small apps, we may use Node Cluster or PM2. Otherwise, we may use a Docker cluster like ECS.

Create a ‘Maintenance Endpoint’

We can use this to securely expose diagnostic information from our app without logging into the server. Some information are just easier to get using code.

In Node, we can use the os module to expose information about our server. For example, we can get the platform for our app as follows and return it via an endpoint:

const express = require('express');
const bodyParser = require('body-parser');
const os = require('os');

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res, next) => {
  res.send(os.platform());
});

app.listen(3000, () => console.log('server started'));
module.exports = app;

Of course, in a production app, this should be secured with authentication.

Conclusion

Before going to production, we should have a set of end to end tests that run in a clean production-like environment. The data should be reset each test run so that they actually run properly. This also helps with testing after refactoring.

We should make sure that we have automated deploy to free our time for other tasks.

Also, make sure that our server’s CPU cores are all utilized.

Finally, we may want to create a secure maintenance endpoint to expose some information to us without logging into the server.