CSRF/XSS protection (#374)

* /api endpoints only allows requests with application/json Content-Type

Otherwise sends 406 Unacceptable

* Uses CSRF token

The CSRF token is sent as the cookie 'XSRF-TOKEN' on all HTML page
requests. This token is  picked up automatically by axios
and sent to the API with all requests as an 'X-XSRF-TOKEN' header.
The middleware runs on all routes and verifies that the token matches
what's stored in the session.
This commit is contained in:
Andrew Nicolaou 2017-06-26 19:58:58 +02:00 committed by Cassie Tarakajian
parent 4476405021
commit 6cbc376d6e
3 changed files with 34 additions and 7 deletions

View file

@ -71,6 +71,7 @@
"cookie-parser": "^1.4.1", "cookie-parser": "^1.4.1",
"cors": "^2.8.1", "cors": "^2.8.1",
"csslint": "^0.10.0", "csslint": "^0.10.0",
"csurf": "^1.9.0",
"decomment": "^0.8.7", "decomment": "^0.8.7",
"dotenv": "^2.0.0", "dotenv": "^2.0.0",
"dropzone": "^4.3.0", "dropzone": "^4.3.0",

View file

@ -7,6 +7,7 @@ import session from 'express-session';
import connectMongo from 'connect-mongo'; import connectMongo from 'connect-mongo';
import passport from 'passport'; import passport from 'passport';
import path from 'path'; import path from 'path';
import csurf from 'csurf';
// Webpack Requirements // Webpack Requirements
import webpack from 'webpack'; import webpack from 'webpack';
@ -23,6 +24,7 @@ import files from './routes/file.routes';
import aws from './routes/aws.routes'; import aws from './routes/aws.routes';
import serverRoutes from './routes/server.routes'; import serverRoutes from './routes/server.routes';
import embedRoutes from './routes/embed.routes'; import embedRoutes from './routes/embed.routes';
import { requestsOfTypeJSON } from './utils/requestsOfType';
import { renderIndex } from './views/index'; import { renderIndex } from './views/index';
import { get404Sketch } from './views/404Page'; import { get404Sketch } from './views/404Page';
@ -73,18 +75,27 @@ app.use(session({
autoReconnect: true autoReconnect: true
}) })
})); }));
// Enables CSRF protection and stores secret in session
app.use(csurf());
// Middleware to add CSRF token as cookie to some requests
const csrfToken = (req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken());
next();
};
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
app.use('/api', users); app.use('/api', requestsOfTypeJSON(), users);
app.use('/api', sessions); app.use('/api', requestsOfTypeJSON(), sessions);
app.use('/api', projects); app.use('/api', requestsOfTypeJSON(), projects);
app.use('/api', files); app.use('/api', requestsOfTypeJSON(), files);
app.use('/api', aws); app.use('/api', requestsOfTypeJSON(), aws);
// this is supposed to be TEMPORARY -- until i figure out // this is supposed to be TEMPORARY -- until i figure out
// isomorphic rendering // isomorphic rendering
app.use('/', serverRoutes); app.use('/', csrfToken, serverRoutes);
app.use('/', embedRoutes); app.use('/', csrfToken, embedRoutes);
app.get('/auth/github', passport.authenticate('github')); app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => { app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
res.redirect('/'); res.redirect('/');

View file

@ -0,0 +1,15 @@
/*
express middleware that sends a 406 Unacceptable
response if an incoming request's Content-Type
header does not match `type`
*/
const requestsOfType = type => (req, res, next) => {
if (req.get('content-type') != null && !req.is(type)) {
return next({ statusCode: 406 }); // 406 UNACCEPTABLE
}
return next();
};
export default requestsOfType;
export const requestsOfTypeJSON = () => requestsOfType('application/json');