Skip to content

Commit 6cbc376

Browse files
andrewncatarak
authored andcommitted
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.
1 parent 4476405 commit 6cbc376

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"cookie-parser": "^1.4.1",
7272
"cors": "^2.8.1",
7373
"csslint": "^0.10.0",
74+
"csurf": "^1.9.0",
7475
"decomment": "^0.8.7",
7576
"dotenv": "^2.0.0",
7677
"dropzone": "^4.3.0",

server/server.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import session from 'express-session';
77
import connectMongo from 'connect-mongo';
88
import passport from 'passport';
99
import path from 'path';
10+
import csurf from 'csurf';
1011

1112
// Webpack Requirements
1213
import webpack from 'webpack';
@@ -23,6 +24,7 @@ import files from './routes/file.routes';
2324
import aws from './routes/aws.routes';
2425
import serverRoutes from './routes/server.routes';
2526
import embedRoutes from './routes/embed.routes';
27+
import { requestsOfTypeJSON } from './utils/requestsOfType';
2628

2729
import { renderIndex } from './views/index';
2830
import { get404Sketch } from './views/404Page';
@@ -73,18 +75,27 @@ app.use(session({
7375
autoReconnect: true
7476
})
7577
}));
78+
79+
// Enables CSRF protection and stores secret in session
80+
app.use(csurf());
81+
// Middleware to add CSRF token as cookie to some requests
82+
const csrfToken = (req, res, next) => {
83+
res.cookie('XSRF-TOKEN', req.csrfToken());
84+
next();
85+
};
86+
7687
app.use(passport.initialize());
7788
app.use(passport.session());
78-
app.use('/api', users);
79-
app.use('/api', sessions);
80-
app.use('/api', projects);
81-
app.use('/api', files);
82-
app.use('/api', aws);
89+
app.use('/api', requestsOfTypeJSON(), users);
90+
app.use('/api', requestsOfTypeJSON(), sessions);
91+
app.use('/api', requestsOfTypeJSON(), projects);
92+
app.use('/api', requestsOfTypeJSON(), files);
93+
app.use('/api', requestsOfTypeJSON(), aws);
8394
// this is supposed to be TEMPORARY -- until i figure out
8495
// isomorphic rendering
85-
app.use('/', serverRoutes);
96+
app.use('/', csrfToken, serverRoutes);
8697

87-
app.use('/', embedRoutes);
98+
app.use('/', csrfToken, embedRoutes);
8899
app.get('/auth/github', passport.authenticate('github'));
89100
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
90101
res.redirect('/');

server/utils/requestsOfType.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
express middleware that sends a 406 Unacceptable
3+
response if an incoming request's Content-Type
4+
header does not match `type`
5+
*/
6+
const requestsOfType = type => (req, res, next) => {
7+
if (req.get('content-type') != null && !req.is(type)) {
8+
return next({ statusCode: 406 }); // 406 UNACCEPTABLE
9+
}
10+
11+
return next();
12+
};
13+
14+
export default requestsOfType;
15+
export const requestsOfTypeJSON = () => requestsOfType('application/json');

0 commit comments

Comments
 (0)