From d407cd970bbd5c0b58b676fdb6c4b60254801c15 Mon Sep 17 00:00:00 2001 From: Rob Scott Date: Mon, 11 Jan 2021 09:09:06 +0000 Subject: [PATCH 1/5] max duration on session --- .gitignore | 1 + README.md | 16 ++++++ index.js | 31 +++++++++++ package.json | 4 +- session/cookie.js | 1 + test/session.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 207febba..5b3ace39 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ coverage node_modules npm-debug.log package-lock.json +.npmrc diff --git a/README.md b/README.md index 38b75c7c..06a00e91 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,22 @@ app.use(session({ })) ``` +##### 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 The name of the session ID cookie to set in the response (and read from in the diff --git a/index.js b/index.js index d7efeab9..a2a31900 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,25 @@ 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,6 +522,9 @@ function session(options) { if (err || !sess) { debug('no session found') generate() + } else if (hasReachedMaxDuration(sess, opts)) { + debug('session has reached the max duration'); + generate(); } else { debug('session found') inflate(req, sess) diff --git a/package.json b/package.json index e3f988f7..7d9a328a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "express-session", + "name": "@varedis/express-session", "version": "1.17.1", "description": "Simple session middleware for Express", "author": "TJ Holowaychuk (http://tjholowaychuk.com)", @@ -7,7 +7,7 @@ "Douglas Christopher Wilson ", "Joe Wagner " ], - "repository": "expressjs/session", + "repository": "varedis/session", "license": "MIT", "dependencies": { "cookie": "0.4.1", 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 8d796f91..f61d9ee2 100644 --- a/test/session.js +++ b/test/session.js @@ -1309,6 +1309,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/) From d841021368c374d268b23075e552a2acb5f71c41 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 21 Feb 2023 04:16:05 +0000 Subject: [PATCH 2/5] fix: upgrade express from 4.17.1 to 4.18.2 Snyk has created this PR to upgrade express from 4.17.1 to 4.18.2. See this package in npm: https://www.npmjs.com/package/express See this project in Snyk: https://app.snyk.io/org/varedis/project/0941684c-978c-4364-9ed9-04b769aece1b?utm_source=github&utm_medium=referral&page=upgrade-pr --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d9a328a..b7213d7d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "cookie-parser": "1.4.5", "eslint": "3.19.0", "eslint-plugin-markdown": "1.0.2", - "express": "4.17.1", + "express": "4.18.2", "mocha": "8.1.1", "nyc": "15.1.0", "supertest": "4.0.2" From 1655421cd3c999c6af972200aa8a867e66993755 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 21 Feb 2023 04:16:10 +0000 Subject: [PATCH 3/5] fix: upgrade cookie-signature from 1.0.6 to 1.2.0 Snyk has created this PR to upgrade cookie-signature from 1.0.6 to 1.2.0. See this package in npm: https://www.npmjs.com/package/cookie-signature See this project in Snyk: https://app.snyk.io/org/varedis/project/0941684c-978c-4364-9ed9-04b769aece1b?utm_source=github&utm_medium=referral&page=upgrade-pr --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d9a328a..97ebb4a9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "cookie": "0.4.1", - "cookie-signature": "1.0.6", + "cookie-signature": "1.2.0", "debug": "2.6.9", "depd": "~2.0.0", "on-headers": "~1.0.2", From 20644cd2ba6285f250785cc50719ff5fc20f91b2 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 21 Feb 2023 04:16:14 +0000 Subject: [PATCH 4/5] fix: upgrade cookie from 0.4.1 to 0.5.0 Snyk has created this PR to upgrade cookie from 0.4.1 to 0.5.0. See this package in npm: https://www.npmjs.com/package/cookie See this project in Snyk: https://app.snyk.io/org/varedis/project/0941684c-978c-4364-9ed9-04b769aece1b?utm_source=github&utm_medium=referral&page=upgrade-pr --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d9a328a..eeb71eb6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "repository": "varedis/session", "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~2.0.0", From 7eb4cd847247f26dc6c120f0cd617136e3a89fd5 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 21 Feb 2023 04:16:18 +0000 Subject: [PATCH 5/5] fix: upgrade cookie-parser from 1.4.5 to 1.4.6 Snyk has created this PR to upgrade cookie-parser from 1.4.5 to 1.4.6. See this package in npm: https://www.npmjs.com/package/cookie-parser See this project in Snyk: https://app.snyk.io/org/varedis/project/0941684c-978c-4364-9ed9-04b769aece1b?utm_source=github&utm_medium=referral&page=upgrade-pr --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d9a328a..a4a1094c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "after": "0.8.2", - "cookie-parser": "1.4.5", + "cookie-parser": "1.4.6", "eslint": "3.19.0", "eslint-plugin-markdown": "1.0.2", "express": "4.17.1",