diff --git a/app.js b/app.js index 15a7a5442..739c735b2 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,7 @@ const passport = require('./services/passport'); const session = require('express-session'); const RedisStore = require('connect-redis')(session); const redis = require('./services/redis'); +const csrf = require('csurf'); const app = express(); @@ -64,6 +65,17 @@ if (app.get('env') === 'production') { app.use(session(session_opts)); +//============================================================================== +// CSRF MIDDLEWARE +//============================================================================== + +app.use(csrf()); + +app.use((err, req, res, next) => { + res.locals._csrf = req.csrfToken(); + return next(); +}); + //============================================================================== // PASSPORT MIDDLEWARE //============================================================================== diff --git a/client/coral-plugin-csrf/FormCSRF.js b/client/coral-plugin-csrf/FormCSRF.js index 8fddcae40..7f555d986 100644 --- a/client/coral-plugin-csrf/FormCSRF.js +++ b/client/coral-plugin-csrf/FormCSRF.js @@ -2,10 +2,10 @@ import React from 'react'; export const FormCSRFInput = React.createClass({ render() { - const token = ''; //$('meta[name="csrf-token"]').attr('content'); + const {csrfToken} = this.props; return ( - + ); } }); diff --git a/routes/admin/index.js b/routes/admin/index.js index d9fb7c1d8..4bce3711f 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,20 +1,15 @@ const express = require('express'); const router = express.Router(); -const csrf = require('csurf'); - -// Setup route middlewares for CSRF protection. -// Default ignore methods are GET, HEAD, OPTIONS -const csrfProtection = csrf({}); // Get /password-reset expects a signed token (JWT) in the hash. // Links to this endpoint are generated by /views/password-reset-email.ejs. -router.get('/password-reset', csrfProtection, (req, res, next) => { +router.get('/password-reset', (req, res, next) => { // TODO: store the redirect uri in the token or something fancy. // admins and regular users should probably be redirected to different places. res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL, csrfToken: req.csrfToken()}); }); -router.get('*', csrfProtection, (req, res) => { +router.get('*', (req, res) => { res.render('admin', {basePath: '/client/coral-admin', csrfToken: req.csrfToken()}); }); diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index 5aa9b8cc6..71545505e 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -4,6 +4,14 @@ const router = express.Router(); const Asset = require('../../../models/asset'); const scraper = require('../../../services/scraper'); +const csrf = require('csurf'); +const bodyParser = require('body-parser'); + +// Setup route middlewares for CSRF protection. +// Default ignore methods are GET, HEAD, OPTIONS +const csrfProtection = csrf({}); +const parseForm = bodyParser.urlencoded({extended: false}); + // List assets. router.get('/', (req, res, next) => { @@ -59,7 +67,7 @@ router.get('/:asset_id', (req, res, next) => { }); // Adds the asset id to the queue to be scraped. -router.post('/:asset_id/scrape', (req, res, next) => { +router.post('/:asset_id/scrape', parseForm, csrfProtection, (req, res, next) => { // Create a new asset scrape job. Asset diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index bcbfcc27e..6d9f5fb49 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -4,6 +4,14 @@ const authorization = require('../../../middleware/authorization'); const router = express.Router(); +const csrf = require('csurf'); +const bodyParser = require('body-parser'); + +// Setup route middlewares for CSRF protection. +// Default ignore methods are GET, HEAD, OPTIONS +const csrfProtection = csrf({}); +const parseForm = bodyParser.urlencoded({extended: false}); + /** * This returns the user if they are logged in. */ @@ -80,7 +88,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { /** * Local auth endpoint, will recieve a email and password */ -router.post('/local', (req, res, next) => { +router.post('/local', parseForm, csrfProtection, (req, res, next) => { // Perform the local authentication. passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next); diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 90b8b40f2..74005dab9 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -7,6 +7,14 @@ const wordlist = require('../../../services/wordlist'); const authorization = require('../../../middleware/authorization'); const _ = require('lodash'); +const csrf = require('csurf'); +const bodyParser = require('body-parser'); + +// Setup route middlewares for CSRF protection. +// Default ignore methods are GET, HEAD, OPTIONS +const csrfProtection = csrf({}); +const parseForm = bodyParser.urlencoded({extended: false}); + const router = express.Router(); router.get('/', (req, res, next) => { @@ -82,7 +90,7 @@ router.get('/', (req, res, next) => { }); }); -router.post('/', wordlist.filter('body'), (req, res, next) => { +router.post('/', parseForm, csrfProtection, wordlist.filter('body'), (req, res, next) => { const { body, @@ -183,7 +191,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next }); }); -router.post('/:comment_id/actions', (req, res, next) => { +router.post('/:comment_id/actions', parseForm, csrfProtection, (req, res, next) => { const { action_type, diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 5b0c48de8..57c90a877 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -9,6 +9,14 @@ const resetEmailFile = fs.readFileSync(path.resolve(__dirname, '../../../views/p const resetEmailTemplate = ejs.compile(resetEmailFile.toString()); const authorization = require('../../../middleware/authorization'); +const csrf = require('csurf'); +const bodyParser = require('body-parser'); + +// Setup route middlewares for CSRF protection. +// Default ignore methods are GET, HEAD, OPTIONS +const csrfProtection = csrf({}); +const parseForm = bodyParser.urlencoded({extended: false}); + router.get('/', authorization.needed('admin'), (req, res, next) => { const { value = '', @@ -38,7 +46,7 @@ router.get('/', authorization.needed('admin'), (req, res, next) => { .catch(next); }); -router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => { +router.post('/:user_id/role', parseForm, csrfProtection, authorization.needed('admin'), (req, res, next) => { User .addRoleToUser(req.params.user_id, req.body.role) .then(() => { @@ -47,7 +55,7 @@ router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => .catch(next); }); -router.post('/:user_id/status', (req, res, next) => { +router.post('/:user_id/status', parseForm, csrfProtection, (req, res, next) => { User .setStatus(req.params.user_id, req.body.status, req.body.comment_id) .then(status => { @@ -56,7 +64,7 @@ router.post('/:user_id/status', (req, res, next) => { .catch(next); }); -router.post('/', (req, res, next) => { +router.post('/', parseForm, csrfProtection, (req, res, next) => { const {email, password, displayName} = req.body; User @@ -78,7 +86,7 @@ ErrPasswordTooShort.status = 400; * 1) the token that was in the url of the email link {String} * 2) the new password {String} */ -router.post('/update-password', (req, res, next) => { +router.post('/update-password', parseForm, csrfProtection, (req, res, next) => { const {token, password} = req.body; if (!password || password.length < 8) { @@ -103,7 +111,7 @@ router.post('/update-password', (req, res, next) => { * this endpoint takes an email (username) and checks if it belongs to a User account * if it does, create a JWT and send an email */ -router.post('/request-password-reset', (req, res, next) => { +router.post('/request-password-reset', parseForm, csrfProtection, (req, res, next) => { const {email} = req.body; if (!email) { @@ -158,7 +166,7 @@ router.put('/:user_id/bio', (req, res, next) => { }); }); -router.post('/:user_id/actions', authorization.needed(), (req, res, next) => { +router.post('/:user_id/actions', parseForm, csrfProtection, authorization.needed(), (req, res, next) => { const { action_type, field, diff --git a/tests/routes/api/auth/index.js b/tests/routes/api/auth/index.js index dd408d135..6bb46b2af 100644 --- a/tests/routes/api/auth/index.js +++ b/tests/routes/api/auth/index.js @@ -2,6 +2,8 @@ const app = require('../../../../app'); const chai = require('chai'); const expect = chai.expect; +const csrf = require('csurf'); + chai.use(require('chai-http')); const User = require('../../../../models/user'); @@ -29,6 +31,7 @@ describe('/api/v1/auth/local', () => { it('should send back the user on a successful login', () => { return chai.request(app) .post('/api/v1/auth/local') + .field('_csrf', req.csrfToken()) .send({email: 'maria@gmail.com', password: 'password!'}) .catch((res) => { expect(res).to.have.status(200); diff --git a/views/password-reset.ejs b/views/password-reset.ejs index 1ffd1b554..13652c1c1 100644 --- a/views/password-reset.ejs +++ b/views/password-reset.ejs @@ -82,6 +82,7 @@