Merge pull request #17732 from overleaf/bg-session-mitigation-initial-protoype

anonymous cookie-based sessions module

GitOrigin-RevId: 75fe2d48fa384ba8d07c0b478a9a5a907a2b3b67
This commit is contained in:
Brian Gough 2024-04-25 16:32:01 +01:00 committed by Copybot
parent a540754f6e
commit 29105911c5
9 changed files with 78 additions and 11 deletions

4
package-lock.json generated
View file

@ -43377,6 +43377,7 @@
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nodemailer": "^6.7.0", "nodemailer": "^6.7.0",
"nodemailer-ses-transport": "^1.5.1", "nodemailer-ses-transport": "^1.5.1",
"on-headers": "^1.0.2",
"openai": "^4.36.0", "openai": "^4.36.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"p-limit": "^2.3.0", "p-limit": "^2.3.0",
@ -43398,6 +43399,7 @@
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tsscmp": "^1.0.6", "tsscmp": "^1.0.6",
"uid-safe": "^2.1.5",
"utf-8-validate": "^5.0.2", "utf-8-validate": "^5.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"valid-data-url": "^2.0.0", "valid-data-url": "^2.0.0",
@ -51977,6 +51979,7 @@
"nodemailer": "^6.7.0", "nodemailer": "^6.7.0",
"nodemailer-ses-transport": "^1.5.1", "nodemailer-ses-transport": "^1.5.1",
"nvd3": "^1.8.6", "nvd3": "^1.8.6",
"on-headers": "^1.0.2",
"openai": "^4.36.0", "openai": "^4.36.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"p-limit": "^2.3.0", "p-limit": "^2.3.0",
@ -52035,6 +52038,7 @@
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tsscmp": "^1.0.6", "tsscmp": "^1.0.6",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"uid-safe": "^2.1.5",
"utf-8-validate": "^5.0.2", "utf-8-validate": "^5.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"valid-data-url": "^2.0.0", "valid-data-url": "^2.0.0",

View file

@ -419,8 +419,9 @@ const AuthenticationController = {
return function (req, res, next) { return function (req, res, next) {
// check that the session store is returning valid results // check that the session store is returning valid results
if (req.session && !SessionStoreManager.hasValidationToken(req)) { if (req.session && !SessionStoreManager.hasValidationToken(req)) {
// force user to update session // Force user to update session by destroying the current one.
req.session.regenerate(() => { // A new session will be created on the next request.
req.session.destroy(() => {
// need to destroy the existing session and generate a new one // need to destroy the existing session and generate a new one
// otherwise they will already be logged in when they are redirected // otherwise they will already be logged in when they are redirected
// to the login page // to the login page

View file

@ -88,10 +88,15 @@ function loadViewIncludes(app) {
} }
} }
function registerAppMiddleware(app) { function registerMiddleware(appOrRouter, middlewareName, options) {
if (!middlewareName) {
throw new Error(
'middleware name must be provided to register module middleware'
)
}
for (const module of modules()) { for (const module of modules()) {
if (module.appMiddleware) { if (module[middlewareName]) {
module.appMiddleware(app) module[middlewareName](appOrRouter, options)
} }
} }
} }
@ -164,7 +169,7 @@ module.exports = {
loadViewIncludes, loadViewIncludes,
moduleIncludes, moduleIncludes,
moduleIncludesAvailable, moduleIncludesAvailable,
registerAppMiddleware, registerMiddleware,
hooks: { hooks: {
attach: attachHook, attach: attachHook,
fire: fireHook, fire: fireHook,

View file

@ -133,7 +133,7 @@ Modules.loadViewIncludes(app)
app.use(metrics.http.monitor(logger)) app.use(metrics.http.monitor(logger))
Modules.registerAppMiddleware(app) Modules.registerMiddleware(app, 'appMiddleware')
app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' })) app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' }))
app.use(bodyParser.json({ limit: Settings.max_json_request_size })) app.use(bodyParser.json({ limit: Settings.max_json_request_size }))
app.use(methodOverride()) app.use(methodOverride())
@ -157,6 +157,9 @@ RedirectManager.apply(webRouter)
webRouter.use(cookieParser(Settings.security.sessionSecret)) webRouter.use(cookieParser(Settings.security.sessionSecret))
SessionAutostartMiddleware.applyInitialMiddleware(webRouter) SessionAutostartMiddleware.applyInitialMiddleware(webRouter)
Modules.registerMiddleware(webRouter, 'sessionMiddleware', {
store: sessionStore,
})
webRouter.use( webRouter.use(
session({ session({
resave: false, resave: false,

View file

@ -71,4 +71,7 @@ module.exports = {
return false return false
} }
}, },
computeValidationToken,
checkValidationToken,
} }

View file

@ -136,6 +136,7 @@
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nodemailer": "^6.7.0", "nodemailer": "^6.7.0",
"nodemailer-ses-transport": "^1.5.1", "nodemailer-ses-transport": "^1.5.1",
"on-headers": "^1.0.2",
"openai": "^4.36.0", "openai": "^4.36.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"p-limit": "^2.3.0", "p-limit": "^2.3.0",
@ -157,6 +158,7 @@
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tsscmp": "^1.0.6", "tsscmp": "^1.0.6",
"uid-safe": "^2.1.5",
"utf-8-validate": "^5.0.2", "utf-8-validate": "^5.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"valid-data-url": "^2.0.0", "valid-data-url": "^2.0.0",

View file

@ -28,6 +28,11 @@ before('start main app', function (done) {
route => route.path && route.path === '/dev/csrf', route => route.path && route.path === '/dev/csrf',
router => { router => {
router.get('/dev/session', (req, res) => { router.get('/dev/session', (req, res) => {
// allow changing the session directly for testing, assign any
// properties in the query string to req.session
if (req.query && Object.keys(req.query).length > 0) {
Object.assign(req.session, req.query)
}
return res.json(req.session) return res.json(req.session)
}) })
} }
@ -75,6 +80,27 @@ before('start main app', function (done) {
}) })
} }
) )
injectRouteAfter(
app,
route => route.path && route.path === '/dev/csrf',
router => {
router.csrf.disableDefaultCsrfProtection(
'/dev/no_autostart_post_gateway',
'POST'
)
router.sessionAutostartMiddleware.disableSessionAutostartForRoute(
'/dev/no_autostart_post_gateway',
'POST',
(req, res, next) => {
next()
}
)
router.post('/dev/no_autostart_post_gateway', (req, res) => {
res.status(200).json({ message: 'no autostart' })
})
}
)
server = App.listen(23000, '127.0.0.1', done) server = App.listen(23000, '127.0.0.1', done)
}) })

View file

@ -8,6 +8,8 @@ const AuthenticationManager = require('../../../../app/src/Features/Authenticati
const { promisifyClass } = require('@overleaf/promise-utils') const { promisifyClass } = require('@overleaf/promise-utils')
const fs = require('fs') const fs = require('fs')
const Path = require('path') const Path = require('path')
const { Cookie } = require('tough-cookie')
const COOKIE_DOMAIN = settings.cookieDomain
let count = settings.test.counterInit let count = settings.test.counterInit
@ -32,10 +34,15 @@ class User {
}) })
} }
getSession(callback) { getSession(options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
this.request.get( this.request.get(
{ {
url: '/dev/session', url: '/dev/session',
qs: options.set,
}, },
(err, response, body) => { (err, response, body) => {
if (err != null) { if (err != null) {
@ -186,6 +193,22 @@ class User {
) )
} }
/* Return the session cookie, url decoded. Use the option {raw:true} to get the original undecoded value */
sessionCookie(options) {
const cookie = Cookie.parse(
this.jar.getCookieString(
// The cookie domain has a leading '.' but
// the cookie jar stores it without.
'https://' + COOKIE_DOMAIN.replace(/^\./, '') + '/'
)
)
if (cookie?.value && !options?.raw) {
cookie.value = decodeURIComponent(cookie.value)
}
return cookie
}
getEmailConfirmationCode(callback) { getEmailConfirmationCode(callback) {
this.getSession((err, session) => { this.getSession((err, session) => {
if (err != null) { if (err != null) {
@ -1224,7 +1247,7 @@ class User {
} }
User.promises = promisifyClass(User, { User.promises = promisifyClass(User, {
without: ['setExtraAttributes'], without: ['setExtraAttributes', 'sessionCookie'],
}) })
User.promises.prototype.doRequest = async function (method, params) { User.promises.prototype.doRequest = async function (method, params) {

View file

@ -624,7 +624,7 @@ describe('AuthenticationController', function () {
beforeEach(function () { beforeEach(function () {
this.req.session = { this.req.session = {
user: this.user, user: this.user,
regenerate: sinon.stub().yields(), destroy: sinon.stub().yields(),
} }
this.req.user = this.user this.req.user = this.user
this.AuthenticationController._redirectToLoginOrRegisterPage = this.AuthenticationController._redirectToLoginOrRegisterPage =
@ -637,7 +637,7 @@ describe('AuthenticationController', function () {
}) })
it('should destroy the current session', function () { it('should destroy the current session', function () {
this.req.session.regenerate.called.should.equal(true) this.req.session.destroy.called.should.equal(true)
}) })
it('should redirect to the register or login page', function () { it('should redirect to the register or login page', function () {