Skip to content

Instantly share code, notes, and snippets.

@cerebrl
Last active August 2, 2023 22:48
Show Gist options
  • Save cerebrl/6487587 to your computer and use it in GitHub Desktop.
Save cerebrl/6487587 to your computer and use it in GitHub Desktop.
Securing ExpressJS

tl;dr

  1. Don't run as root.
  2. For sessions, set httpOnly (and secure to true if running over SSL) when setting cookies.
  3. Use the Helmet for secure headers: https://github.com/evilpacket/helmet
  4. Enable csrf for preventing Cross-Site Request Forgery: http://expressjs.com/api.html#csrf
  5. Don't use the deprecated bodyParser() and only use multipart explicitly. To avoid multiparts vulnerability to 'temp file' bloat, use the defer property and pipe() the multipart upload stream to the intended destination.

Here is a starting guide for securing express.js applications, specifically Express v3. It is by no means a comprehensive guide on web application security. Standard rules and practices apply to express.js apps just as if they would to Rails, Django or any other web application.

I’m going to hit the high points of items that always seem to come up.

Don’t run as root

It’s been long foretold by the ancient bearded ops that one shall run a service with the least amount of privilege necessary and no more. However this ancient folklore seems to be forgotten from time to time when less experienced devs run into the obvious problem of running their new webapp on ports 80 and 443. Running as root solves this quickly and they can move on to other, more fun challenges.

One way to approach this is to drop process privileges after you bind to the port using something like this:

http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
    process.setgid(config.gid);
    process.setuid(config.uid);
});

Note: As Joshua Heiks points out in the comments below the gid should be set before the uid.

There are a couple caveats to this. It’s not available on Windows and if you drop privileges before your bind actually finishes you could run into issues, but to be honest, I have never had this happen.

Another is to use something like authbind or by putting something like nginx or another proxy in front of your application. Whatever you do, just don’t freak’n run as root.

Sessions

Most express apps are going to deal with user sessions at some point.

Session cookies should have the SECURE and HTTPOnly flags set. This ensures they can only be sent over HTTPS (you are using HTTPS, right?) and there is no script access to the cookie client side.

app.use(express.session({
  secret: "notagoodsecretnoreallydontusethisone",
  cookie: {httpOnly: true, secure: true},
}));

Security Headers

There are plenty of security headers that help improve security with just a line or two of code. I’m not going to explain them all, but you should read and familiarize yourself with them. A great article to read is Seven Web Server HTTP Headers that Improve Web Application Security for Free

The easiest way to implement most of these headers in Express is to use the helmet middleware.

npm install helmet

Then we can add them to our app.configure for express

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());  // Deprecated, do not use. See 3-do-not-use-bodyparser.md below
  app.use(helmet.xframe());
  app.use(helmet.iexss());
  app.use(helmet.contentTypeOptions());
  app.use(helmet.cacheControl());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({
    secret: "notagoodsecret",
    cookie: {httpOnly: true, secure: true},
  }));
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});

Cross-Site Request Forgery (CSRF) Protection

Express provides CSRF protection using built in middleware. It’s not enabled by default. Documentation for the express.csrf() middleware is available here.

To enable CSRF protection let’s add it to the app.configure section. It should come after the session parser and before the router.

The first line we add is to add csrf tokens to the users session.

app.use(express.csrf());

Then, since Express v3 did away with dynamic helpers, we use a small middleware to add the token to our locals making it available to templates.

  app.use(function (req, res, next) {
    res.locals.csrftoken = req.session._csrf;
    next();
  });

The final example, putting it together:

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());  // Deprecated, do not use. See 3-do-not-use-bodyparser.md below
  app.use(helmet.xframe());
  app.use(helmet.iexss());
  app.use(helmet.contentTypeOptions());
  app.use(helmet.cacheControl());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({
    secret: "notagoodsecret",
    cookie: {httpOnly: true},
  }));
  app.use(express.csrf());
  app.use(function (req, res, next) {
    res.locals.csrftoken = req.session._csrf;
    next();
  });
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});

Here is an example of using the csrf token in a jade template:

form(method="post",action="/login")
  input(type="hidden", name="_csrf", value="#{csrftoken}")
  button(type="submit") Login

NOTE: I removed the secure: true for this example so it would work without SSL if you wanted to test it out.

Those are just a few things to get you started securing your Express app. Chances are you will be doing that in every app you create, so I created the express-secure-skeleton app to make playing around with these security features a bit easier. Please fork and contribute.

Demonstration

This problem is extremely easy to demonstrate. Here's a simple express app:

var express = require('express');
var app = express();

app.use(express.bodyParser());
app.post('/test', function(req, resp) {
  resp.send('ok');
});

app.listen(9001);

Seems pretty innocuous right?

Now check how many temp files you have with something like this:

$ ls /tmp | wc -l
> 33

Next simulate uploading a multipart form:

$ curl -X POST -F foo=@tmp/somefile.c http://localhost:9001/test
> ok

Go back and check our temp file count:

$ ls /tmp | wc -l
>34

That's a problem.

Solutions

Always delete the temp files when you use bodyParser or multipart middleware

You can prevent this attack by always checking whether req.files is present for endpoints in which you use bodyParser or multipart, and then deleting the temp files. Note that this is every POST endpoint if you did something like app.use(express.bodyParser()).

This is suboptimal for several reasons:

It is too easy to forget to do these checks.

It requires a bunch of ugly cleanup code. Why have code when you could not have code? Your server is still, for every POST endpoint that you use bodyParser, processing every multipart upload that comes its way, creating a temp file, writing it to disk, and then deleting the temp file. Why do all that when you don't want to accept uploads?

As of express 3.4.0 (connect 2.9.0) bodyParser is deprecated. It goes without saying that deprecated things should be avoided.

Use a utility such as tmpwatch or reap

jfromaniello pointed out that using a utility such as tmpwatch can help with this issue. The idea here is to, for example, schedule tmpwatch as a cron job. It would remove temp files that have not been accessed in a long enough period of time.

It's usually a good idea to do this for all servers, just in case. But relying on this to clean up bodyParser's mess still suffers from issue #3 outlined above. Plus, server hard drives are often small, especially when you didn't realize you were going to have temp files in the first place.

If you ran your cron job every 8 hours for instance, given a hdd with 4 GB of free space, an attacker would need an Internet connection with 145 KB/s upload bandwidth to crash your server.

TJ pointed out that he also has a utility for this purpose called reap.

Avoid bodyParser and explicitly use the middleware that you need

If you want to parse json in your endpoint, use express.json() middleware. If you want json and urlencoded endpoint, use [express.json(), express.urlencoded()] for your middleware.

If you want users to upload files to your endpoint, you could use express.multipart() and be sure to clean up all the temp files that are created. This would still stuffer from problem #3 previously mentioned.

Use the defer option in the multipart middleware

When you create your multipart middleware, you can use the defer option like this:

express.multipart({defer: true})

According to the documentation:

defers processing and exposes the multiparty form object as req.form. next() is called without waiting for the form's "end" event. This option is useful if you need to bind to the "progress" or "part" events, for example. So if you do this you will use multiparty's API assuming that req.form is an instantiated Form instance.

Use an upload parsing module directly

bodyParser depends on multipart, which behind the scenes uses multiparty to parse uploads.

You can use this module directly to handle the request. In this case you can look at multiparty's API and do the right thing.

There are also alternatives such as busboy, parted, and formidable.

Express 3.4.0 and Connect 2.9.0 have made some small changes to bodyParser(), and more specifically the multipart() middleware used within it. There have been concerns regarding temporary-file usage, however to maintain backwards compatibility for now I've added some documentation.

We've also switched to the "multiparty" library, instead of using formidable, which allows you to stream the parts directly to arbitrary destinations without hitting disk. Keep in mind that the destination streams must properly implement node's backpressure mechanisms otherwise you're likely to cause large memory bloat causing the process to fail. The "defer" option let's subsequent middleware listen on "part" events to stream accordingly instead of writing to disk, providing the convenient req.files object that you might be used to.

Another alternative if you're concerned is to simply use express.json(), and express.urlencoded(), and leave out multipart() all together. Use if (req.is('multipart/form-data') and formidable, multipartyor parted directly.

The tmpfile used is os.tmpDir()'s value, so if you plan on continuing to use disk it's highly recommended to set up a strategy for dealing with unnecessary temporary files, this is good practice for any production environment, much like log rotation it is critical to any large deployment. An example tool is [reap(1)](https://github.com/visionmedia/reap). Tools like this should be used regardless of the cleanup technique, as application processes may fail at any point in time, and may never have the chance to unlink()` the file.

The default limits for bodyParser(), urlencoded(), multipart() and json() have also been adjusted. The default limit for multipart is now 100mb, and 1mb for the other two. If you anticipate requests larger than this you may pass { limit: '200mb' } to either bodyParser() or the others. It's recommended to use each one individually, bodyParser() is a legacy convenience aggregate of the others, but applying a global .limit option between the three of them is not a great choice, as sending 200mb of JSON could halt the application.

If node sits behind a reverse proxy such as nginx you may easily tweak this behaviour there as well.

If you have questions, concerns, or suggestions let me know.

When defer is used files are not streamed to tmpfiles, you may access them via the "part" events and stream them accordingly:

req.form.on('part', function(part){
  // transfer to s3 etc
  console.log('upload %s %s', part.name, part.filename);
  var out = fs.createWriteStream('/tmp/' + part.filename);
  part.pipe(out);
});

req.form.on('close', function(){
  res.end('uploaded!');
});
@rfdslabs
Copy link

Thanks!

@felixmc
Copy link

felixmc commented Jan 20, 2015

pretty useful, but could probably use an update.

@Agrejus
Copy link

Agrejus commented Jun 15, 2017

Agreed, very nice. An update would be awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment