diff --git a/README.md b/README.md index 11026f8..5ede70c 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ following keys: - `key` - the name of the cookie to use to store the token secret (defaults to `'_csrf'`). - `path` - the path of the cookie (defaults to `'/'`). + - `maxAge` - the maximum age the cookie can be before it expires in seconds. Setting this parameter will cause a 403 response if any cookie and csfr token is sent beyond the maxAge window. - any other [res.cookie](http://expressjs.com/4x/api.html#res.cookie) option can be set. @@ -116,7 +117,7 @@ var bodyParser = require('body-parser') var express = require('express') // setup route middlewares -var csrfProtection = csrf({ cookie: true }) +var csrfProtection = csrf({ cookie: { maxAge: 60 * 60 * 8 } }) var parseForm = bodyParser.urlencoded({ extended: false }) // create express app @@ -220,7 +221,7 @@ app.use('/api', api) // now add csrf and other middlewares, after the "/api" was mounted app.use(bodyParser.urlencoded({ extended: false })) app.use(cookieParser()) -app.use(csrf({ cookie: true })) +app.use(csrf({ cookie: { maxAge: 60 * 60 * 8 } })) app.get('/form', function (req, res) { // pass the csrfToken to the view diff --git a/index.js b/index.js index c7a6920..038028c 100644 --- a/index.js +++ b/index.js @@ -89,6 +89,7 @@ function csurf (options) { // generate & set new secret if (sec === undefined) { sec = tokens.secretSync() + sec = setExpiry(sec, cookie) setSecret(req, res, sessionKey, sec, cookie) } @@ -104,11 +105,12 @@ function csurf (options) { // generate & set secret if (!secret) { secret = tokens.secretSync() + secret = setExpiry(secret, cookie) setSecret(req, res, sessionKey, secret, cookie) } // verify the incoming token - if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) { + if (!ignoreMethod[req.method] && (!tokens.verify(secret, value(req)) || !verifyExpiry(secret, cookie))) { return next(createError(403, 'invalid csrf token', { code: 'EBADCSRFTOKEN' })) @@ -232,6 +234,25 @@ function getSecretBag (req, sessionKey, cookie) { } } +/** + * Set an expiry time on the cookie. + * + * @param {string} val + * @param {Object} [options] + * @api private + */ + +function setExpiry (val, options) { + var secret = val + if (options) { + if (options.maxAge) { + var time = ((new Date().getTime() + (options.maxAge * 1000)).toString(21)) + secret += ('-' + time) + } + } + return secret +} + /** * Set a cookie on the HTTP response. * @@ -244,7 +265,6 @@ function getSecretBag (req, sessionKey, cookie) { function setCookie (res, name, val, options) { var data = Cookie.serialize(name, val, options) - var prev = res.getHeader('set-cookie') || [] var header = Array.isArray(prev) ? prev.concat(data) : Array.isArray(data) ? [prev].concat(data) @@ -289,6 +309,22 @@ function setSecret (req, res, sessionKey, val, cookie) { throw new Error('misconfigured csrf') } } +/** + * Verify the cookie/token has not expired. + * @private + */ +function verifyExpiry (secret, cookie) { + if (cookie) { + if (cookie.maxAge) { + var index = secret.lastIndexOf('-') + if (index === -1) { return false } + var time = secret.substr(index + 1, secret.length - index) + time = parseInt(time, 21) + return (new Date().getTime()) < time + } + } + return true +} /** * Verify the configuration against the request. diff --git a/test/test.js b/test/test.js index 1e14d36..12719e7 100644 --- a/test/test.js +++ b/test/test.js @@ -1,4 +1,3 @@ - process.env.NODE_ENV = 'test' var assert = require('assert') @@ -262,11 +261,11 @@ describe('csurf', function () { describe('with "ignoreMethods" option', function () { it('should reject invalid value', function () { - assert.throws(createServer.bind(null, {ignoreMethods: 'tj'}), /option ignoreMethods/) + assert.throws(createServer.bind(null, { ignoreMethods: 'tj' }), /option ignoreMethods/) }) it('should not check token on given methods', function (done) { - var server = createServer({ignoreMethods: ['GET', 'POST']}) + var server = createServer({ ignoreMethods: ['GET', 'POST'] }) request(server) .get('/') @@ -346,7 +345,7 @@ describe('csurf', function () { }) app.use('/new', function (req, res, next) { // regenerate session - req.session = {hit: 1} + req.session = { hit: 1 } next() }) app.use(function (req, res) { @@ -388,6 +387,36 @@ describe('csurf', function () { .expect(500, /misconfigured csrf/, done) }) }) + describe('when using "maxAge" cookie option', function () { + var server = createServer({ cookie: { maxAge: 3600 } }) + it('should work in with valid token', function (done) { + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('csrf-token', token) + .expect(200, done) + }) + }) + it('should reject expired tokens', function (done) { + var server = createServer({ cookie: { maxAge: -3600 } }) + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('csrf-token', token) + .expect(403, done) + }) + }) + }) }) function cookie (res, name) { @@ -415,7 +444,7 @@ function createServer (opts) { req.query = url.parse(req.url, true).query next() }) - app.use(bodyParser.urlencoded({extended: false})) + app.use(bodyParser.urlencoded({ extended: false })) app.use(csurf(opts)) app.use(function (req, res) {