Security in Node.JS and Express: The bare minimum - Part 3.
Petros Demetrakopoulos
Posted on April 15, 2020
In the previous part, we covered
- XSS Attacks
- SQL injections
- RegEx Denial of Service
Security in Node.JS and Express: The bare minimum - Part 2.
Petros Demetrakopoulos ・ Apr 10 '20 ・ 3 min read
In this part, we will cover
- Cross-Site Request Forgery Attacks (CSRF)
- Rate Limiting
- Data Sanitization
Cross-Site Request Forgery
Cross-Site Request Forgery according to OWASP
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request.
In order to prevent this kind of attacks, we should implement a synchronized CSRF tokens policy.
CSRF token is a simple string set when the user requests a page that contains a form and expects the same CSRF token when a POST request is made. If the CSRF tokens do not match or if the CSRF token is not in the form data, the POST request is not allowed. CSRF token is unique for each user session and most of the times it expires in a given time span.
In Express applications we can implement a CSRF policy with the help of csurf npm package.
The package can be used in one line and it handles everything related to the CSRF tokens for all the users.
So in the back-end the correct setup looks like this
var csrf = require('csurf');
var app = express();
app.use(csrf());
app.use(function(req, res, next) {
res.locals._csrf = req.csrfToken();
next();
});
And in the front-end looks like this for each form.
<html>
<form method="post" action=“changeEmail">
<input type="hidden" name="_csrf" value="_csrf">
<input type="email" name=“newEmail">
</form>
</html>
Rate Limiting
One other crucial aspect of the security of your Express application is rate limiting. As you may already know, rate limiting is the policy that control the rate of requests that your server can receive from a specific user and / or IP address. In that way we prevent DoS attacks.
express-rate-limit npm package enables us to apply policies like the ones mentioned above in a really easy way.
i.e
var rateLimit = require("express-rate-limit");
app.set('trust proxy', 1); // add this line only if your server is behind a proxy
var limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
delayMs: 0 // disable delaying - user has full speed until the max limit is reached
});
app.use(limiter); // applies rate limiting policy to every endpoint of the server
// we could also apply policies for specific routes or even different policies for each route
express-rate-limit allows us to apply rate limiting policies to all the endpoints of our Express server or even different policies for each route.
i.e This example applies a rate limiting policy only to the enpoints starting with /api.
var rateLimit = require("express-rate-limit");
var apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100
});
// only apply to requests that begin with /api/
app.use("/api/", apiLimiter);
Important note: Static resources such as images, CSS stylesheets, front-end Javascript scripts count for requests as well if we serve them through our Express server (which is a bad practice anyway, we should prefer CDN networks for static resources).
Data sanitisation and validation
It is an important process that must take place in every endpoint where the user interacts with the server by submitting data. It protects the server from most of the flaws mentioned in this serie of articles. When we are validating data, we are interested in checks like "Is this a correct e-mail address?", "Is it an Integer?", "Is it a valid telephone number?" etc.
A very usefull npm package that helps us perform this kind of checks in user input is express-validator.
express-validator allows us to define "check schemas" for each endpoint in pure JSON. It also allows us to set the error messages sent back to the user if a validation for a field fails.
An example is given below:
app.put('/user/:id/password', checkSchema({
id: {
// the location of the field can be one or more of 'body', 'cookies',
'headers', 'params' or 'query'.
// If omitted, all request locations will be checked
in: ['params','query'],
isInt: true,
errorMessage: 'ID is wrong'
},
password: {
isLength: {
errorMessage: 'Password should be at least 7 characters long',
options: {min: 7}
}
}
})
express-validator offers many useful keys and functions such as isIn()
, exists()
, isUUID()
, isPostalCode()
, trimimng functions etc. It also allows us to implement custom validation and sanitisation logics.
That's all folks (for now...)
I hope you find it interesting and it will help you build more secure and robust Node.JS and Express apps.
Posted on April 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.