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.