diff --git a/README.md b/README.md index 01477c39..920d8439 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,21 @@ app.use(session({ secret: 'keyboard cat' })) ``` +##### maxDuration + +The maximum amount of time that a session can stay open *even* if there are continuous +requests that keep the session alive. This is to minimize the total footprint of a session +replay attack in the case where a session identifier is stolen. This will treat the session as +expired and generate a new session. Rolling sessions do not update this behavior. + +The default is none. + +```js +app.use(session({ + maxDuration: 28800, // duration in seconds (this would be 8 hours) + secret: 'keyboard cat' +})) +``` ##### name diff --git a/index.js b/index.js index d7efeab9..efbc35fa 100644 --- a/index.js +++ b/index.js @@ -80,6 +80,7 @@ var defer = typeof setImmediate === 'function' * @param {String|Array} [options.secret] Secret for signing session ID * @param {Object} [options.store=MemoryStore] Session store * @param {String} [options.unset] + * @param {Number} [options.maxDuration] Sets the maximum total age in seconds for a session to minimize session replay duration * @return {Function} middleware * @public */ @@ -143,6 +144,10 @@ function session(options) { secret = [secret]; } + if (opts.maxDuration && typeof opts.maxDuration !== 'number') { + throw new TypeError('maxDuration needs to be specified as a number'); + } + if (!secret) { deprecate('req.secret; provide secret option'); } @@ -163,6 +168,10 @@ function session(options) { if (cookieOptions.secure === 'auto') { req.session.cookie.secure = issecure(req, trustProxy); } + + if (opts.maxDuration > 0) { + req.session.cookie.createdAt = new Date(); + } }; var storeImplementsTouch = typeof store.touch === 'function'; @@ -472,6 +481,24 @@ function session(options) { : rollingSessions || req.session.cookie.expires != null && isModified(req.session); } + // if opts.maxDuration is set, check to see if the createdAt value of the cookie is has + // expired. + function hasReachedMaxDuration (sess, opts) { + if (!opts || !opts.maxDuration || opts.maxDuration <= 0) { + return false; + } + if (!sess || !sess.cookie || !sess.cookie.createdAt) { + debug('session should be timed out, but the createdAt value is not saved') + return true; + } + var createdDate = new Date(sess.cookie.createdAt); + var nowDate = new Date(); + if ((nowDate.getTime() - createdDate.getTime()) / 1000 < opts.maxDuration) { + return false; + } + return true; + } + // generate a session if the browser doesn't send a sessionID if (!req.sessionID) { debug('no SID sent, generating session'); @@ -494,7 +521,9 @@ function session(options) { if (err || !sess) { debug('no session found') generate() - } else { + } else if (hasReachedMaxDuration(sess, opts)){ + debug('session has reached max duration'); + }else { debug('session found') inflate(req, sess) } diff --git a/package.json b/package.json index d085f292..cd4b4ff8 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "express-session", - "version": "1.17.2", + "name": "tweaked-express-session", + "version": "1.17.3", "description": "Simple session middleware for Express", "author": "TJ Holowaychuk (http://tjholowaychuk.com)", "contributors": [ "Douglas Christopher Wilson ", "Joe Wagner " ], - "repository": "expressjs/session", + "repository": "ghprud/session", "license": "MIT", "dependencies": { "cookie": "0.4.1", @@ -23,9 +23,9 @@ "after": "0.8.2", "cookie-parser": "1.4.5", "eslint": "7.26.0", - "eslint-plugin-markdown": "2.1.0", + "eslint-plugin-markdown": "^2.2.1", "express": "4.17.1", - "mocha": "8.4.0", + "mocha": "^9.1.3", "nyc": "15.1.0", "supertest": "6.1.3" }, diff --git a/session/cookie.js b/session/cookie.js index a8b4e570..6b4546ee 100644 --- a/session/cookie.js +++ b/session/cookie.js @@ -123,6 +123,7 @@ Cookie.prototype = { , domain: this.domain , path: this.path , sameSite: this.sameSite + , createdAt: this.createdAt } }, diff --git a/test/session.js b/test/session.js index 7ce3c194..cb5c90ac 100644 --- a/test/session.js +++ b/test/session.js @@ -1260,6 +1260,141 @@ describe('session()', function(){ }) }) + describe('maxDuration option', function () { + it('should reject unknown values', function () { + assert.throws(createServer.bind(null, { maxDuration: "baddata" }), /maxDuration needs to be specified as a number/); + }) + it('should keep session in maxDuration', function(done) { + var store = new session.MemoryStore(); + var server = createServer({store: store, maxDuration: 10}, function(req, res) { + req.session.user = 'bob'; + res.write('hello, world'); + res.end(); + }); + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, function(err, res) { + if (err) return done(err); + var originalExpires = expires(res); + setTimeout(function() { + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done); + }, 100); + }); + }); + + // it('should start a new session if createdAt is corrupt', function(done) { + // var store = new session.MemoryStore(); + // var server = createServer({store: store, maxDuration: 10}, function(req, res) { + // req.session.user = 'bob'; + // req.session.cookie.createdAt = null; + // res.write('hello, world'); + // res.end(); + // }); + // request(server) + // .get('/') + // .expect(shouldSetCookie('connect.sid')) + // .expect(200, function(err, res) { + // if (err) return done(err); + // var originalCookie = cookie(res); + // setTimeout(function() { + // request(server) + // .get('/') + // .set('Cookie', cookie(res)) + // .expect(shouldSetCookie('connect.sid')) + // .expect(function (res) { assert.notEqual(originalCookie, cookie(res)); }) + // .expect(200, done); + // }, 100); + // }); + // }); + + // it('it should handle a user mucking up the createdAt date', function(done) { + // var store = new session.MemoryStore(); + // var server = createServer({store: store, maxDuration: 10}, function(req, res) { + // req.session.user = 'bob'; + // req.session.cookie.createdAt = 'some really bad data'; + // res.write('hello, world'); + // res.end(); + // }); + // request(server) + // .get('/') + // .expect(shouldSetCookie('connect.sid')) + // .expect(200, function(err, res) { + // if (err) return done(err); + // var originalCookie = cookie(res); + // setTimeout(function() { + // request(server) + // .get('/') + // .set('Cookie', cookie(res)) + // .expect(shouldSetCookie('connect.sid')) + // .expect(function (res) { assert.notEqual(originalCookie, cookie(res)); }) + // .expect(200, done); + // }, 100); + // }); + // }); + + // it('should destroy session after maxDuration', function(done) { + // var store = new session.MemoryStore(); + // var server = createServer({ store: store, maxDuration: .1 }, function(req, res) { + // req.session.user = 'bob'; + // res.write('hello, world'); + // res.end(); + // }); + // request(server) + // .get('/') + // .expect(shouldSetCookie('connect.sid')) + // .expect(200, function (err, res) { + // if (err) return done(err); + // var originalCookie = cookie(res); + // setTimeout(function () { + // request(server) + // .get('/') + // .set('Cookie', cookie(res)) + // .expect(shouldSetCookie('connect.sid')) + // .expect(function (res) { assert.notEqual(originalCookie, cookie(res)); }) + // .expect(200, done); + // }, 200); + // }); + // }); + + // it('should destroy session after maxDuration even with rolling sessions', function (done) { + // var store = new session.MemoryStore(); + // var server = createServer({ store: store, maxDuration: .1, rolling: true }, function(req, res) { + // req.session.user = 'bob'; + // res.write('hello, world'); + // res.end(); + // }); + // request(server) + // .get('/') + // .expect(shouldSetCookie('connect.sid')) + // .expect(200, function (err, res) { + // if (err) return done(err); + // var originalCookie = cookie(res); + // setTimeout(function () { + // request(server) + // .get('/') + // .set('Cookie', cookie(res)) + // .expect(shouldSetCookie('connect.sid')) + // .expect(200, function (err, res) { + // if (err) return done(err); + // setTimeout(function () { + // request(server) + // .get('/') + // .set('Cookie', cookie(res)) + // .expect(shouldSetCookie('connect.sid')) + // .expect(function (res) { assert.notEqual(originalCookie, cookie(res)); }) + // .expect(200, done); + // }, 100); + // }); + // }, 100); + // }); + // }); + }); + describe('unset option', function () { it('should reject unknown values', function(){ assert.throws(session.bind(null, { unset: 'bogus!' }), /unset.*must/)