- Don't run as root.
- For sessions, set
httpOnly
(andsecure
totrue
if running over SSL) when setting cookies. - Use the Helmet for secure headers: https://github.com/evilpacket/helmet
- Enable
csrf
for preventing Cross-Site Request Forgery: http://expressjs.com/api.html#csrf - Don't use the deprecated
bodyParser()
and only use multipart explicitly. To avoid multiparts vulnerability to 'temp file' bloat, use thedefer
property andpipe()
the multipart upload stream to the intended destination.
-
-
Save tkt028/d12c2e762c473548ffd7 to your computer and use it in GitHub Desktop.
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.
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.
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},
}));
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')));
});
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.
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.
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.
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.
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.
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.
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!');
});