Merge pull request #18088 from overleaf/ab-session-secret-rotation

[web/realtime/history-v1] Support session secret rotation

GitOrigin-RevId: 3c2fa27b1b3e0a8e0c9d1af2e616ce873d54aedf
This commit is contained in:
Brian Gough 2024-05-23 15:51:27 +01:00 committed by Copybot
parent a4816d6e75
commit 344b4d0fa0
11 changed files with 41 additions and 70 deletions

60
package-lock.json generated
View file

@ -18079,6 +18079,7 @@
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"dependencies": { "dependencies": {
"object-assign": "^4", "object-assign": "^4",
"vary": "^1" "vary": "^1"
@ -42416,8 +42417,6 @@
"check-types": "^11.1.2", "check-types": "^11.1.2",
"command-line-args": "^3.0.3", "command-line-args": "^3.0.3",
"config": "^1.19.0", "config": "^1.19.0",
"cookie-parser": "~1.4.5",
"cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"generic-pool": "^2.1.1", "generic-pool": "^2.1.1",
@ -43962,7 +43961,7 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.6",
"express": "^4.19.2", "express": "^4.19.2",
"express-session": "^1.17.1", "express-session": "^1.17.1",
"joi": "^17.12.0", "joi": "^17.12.0",
@ -44457,7 +44456,7 @@
"content-disposition": "^0.5.0", "content-disposition": "^0.5.0",
"contentful": "^10.8.5", "contentful": "^10.8.5",
"cookie": "^0.2.3", "cookie": "^0.2.3",
"cookie-parser": "1.3.5", "cookie-parser": "1.4.6",
"core-js": "^3.30.2", "core-js": "^3.30.2",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"csurf": "^1.11.0", "csurf": "^1.11.0",
@ -45466,31 +45465,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"services/web/node_modules/cookie-parser": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
"integrity": "sha1-nXVVcPtdF4kHcSJ6AjFNm+fPg1Y=",
"dependencies": {
"cookie": "0.1.3",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"services/web/node_modules/cookie-parser/node_modules/cookie": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz",
"integrity": "sha1-5zSlwUF/zkctWu+Cw4HKu2TRpDU=",
"engines": {
"node": "*"
}
},
"services/web/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"services/web/node_modules/csv": { "services/web/node_modules/csv": {
"version": "6.2.5", "version": "6.2.5",
"resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz",
@ -52554,7 +52528,7 @@
"chai": "^4.3.6", "chai": "^4.3.6",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.6",
"cookie-signature": "^1.1.0", "cookie-signature": "^1.1.0",
"express": "^4.19.2", "express": "^4.19.2",
"express-session": "^1.17.1", "express-session": "^1.17.1",
@ -53060,7 +53034,7 @@
"content-disposition": "^0.5.0", "content-disposition": "^0.5.0",
"contentful": "^10.8.5", "contentful": "^10.8.5",
"cookie": "^0.2.3", "cookie": "^0.2.3",
"cookie-parser": "1.3.5", "cookie-parser": "1.4.6",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"core-js": "^3.30.2", "core-js": "^3.30.2",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
@ -53732,27 +53706,6 @@
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
} }
}, },
"cookie-parser": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
"integrity": "sha1-nXVVcPtdF4kHcSJ6AjFNm+fPg1Y=",
"requires": {
"cookie": "0.1.3",
"cookie-signature": "1.0.6"
},
"dependencies": {
"cookie": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz",
"integrity": "sha1-5zSlwUF/zkctWu+Cw4HKu2TRpDU="
}
}
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"csv": { "csv": {
"version": "6.2.5", "version": "6.2.5",
"resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz",
@ -61672,6 +61625,7 @@
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"requires": { "requires": {
"object-assign": "^4", "object-assign": "^4",
"vary": "^1" "vary": "^1"
@ -72400,8 +72354,6 @@
"check-types": "^11.1.2", "check-types": "^11.1.2",
"command-line-args": "^3.0.3", "command-line-args": "^3.0.3",
"config": "^1.19.0", "config": "^1.19.0",
"cookie-parser": "~1.4.5",
"cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"generic-pool": "^2.1.1", "generic-pool": "^2.1.1",

View file

@ -9,12 +9,10 @@ const config = require('config')
const Events = require('events') const Events = require('events')
const BPromise = require('bluebird') const BPromise = require('bluebird')
const express = require('express') const express = require('express')
const cors = require('cors')
const helmet = require('helmet') const helmet = require('helmet')
const HTTPStatus = require('http-status') const HTTPStatus = require('http-status')
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const Metrics = require('@overleaf/metrics') const Metrics = require('@overleaf/metrics')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const swaggerTools = require('swagger-tools') const swaggerTools = require('swagger-tools')
const swaggerDoc = require('./api/swagger') const swaggerDoc = require('./api/swagger')
@ -42,8 +40,6 @@ app.use(
extended: false, extended: false,
}) })
) )
app.use(cookieParser())
app.use(cors())
security.setupSSL(app) security.setupSSL(app)
security.setupBasicHttpAuthForSwaggerDocs(app) security.setupBasicHttpAuthForSwaggerDocs(app)

View file

@ -21,8 +21,6 @@
"check-types": "^11.1.2", "check-types": "^11.1.2",
"command-line-args": "^3.0.3", "command-line-args": "^3.0.3",
"config": "^1.19.0", "config": "^1.19.0",
"cookie-parser": "~1.4.5",
"cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"generic-pool": "^2.1.1", "generic-pool": "^2.1.1",

View file

@ -56,7 +56,17 @@ const io = require('socket.io').listen(server, {
// Bind to sessions // Bind to sessions
const sessionStore = new RedisStore({ client: sessionRedisClient }) const sessionStore = new RedisStore({ client: sessionRedisClient })
const cookieParser = CookieParser(Settings.security.sessionSecret)
if (!Settings.security.sessionSecret) {
throw new Error('No SESSION_SECRET provided.')
}
const sessionSecrets = [
Settings.security.sessionSecret,
Settings.security.sessionSecretUpcoming,
Settings.security.sessionSecretFallback,
].filter(Boolean)
const cookieParser = CookieParser(sessionSecrets)
const sessionSockets = new SessionSockets( const sessionSockets = new SessionSockets(
io, io,

View file

@ -105,7 +105,9 @@ const settings = {
}, },
security: { security: {
sessionSecret: process.env.SESSION_SECRET || 'secret-please-change', sessionSecret: process.env.SESSION_SECRET,
sessionSecretUpcoming: process.env.SESSION_SECRET_UPCOMING,
sessionSecretFallback: process.env.SESSION_SECRET_FALLBACK,
}, },
cookieName: process.env.COOKIE_NAME || 'overleaf.sid', cookieName: process.env.COOKIE_NAME || 'overleaf.sid',

View file

@ -2,4 +2,9 @@ module.exports = {
errors: { errors: {
catchUncaughtErrors: false, catchUncaughtErrors: false,
}, },
security: {
sessionSecret: 'static-secret-for-tests',
sessionSecretFallback: 'static-secret-fallback-for-tests',
},
} }

View file

@ -27,7 +27,7 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.6",
"express": "^4.19.2", "express": "^4.19.2",
"express-session": "^1.17.1", "express-session": "^1.17.1",
"joi": "^17.12.0", "joi": "^17.12.0",

View file

@ -155,10 +155,16 @@ if (Settings.useHttpPermissionsPolicy) {
RedirectManager.apply(webRouter) RedirectManager.apply(webRouter)
if (!Settings.security.sessionSecret) { if (!Settings.security.sessionSecret) {
throw new Error('Session secret is not set - refusing to start server') throw new Error('No SESSION_SECRET provided.')
} }
webRouter.use(cookieParser(Settings.security.sessionSecret)) const sessionSecrets = [
Settings.security.sessionSecret,
Settings.security.sessionSecretUpcoming,
Settings.security.sessionSecretFallback,
].filter(Boolean)
webRouter.use(cookieParser(sessionSecrets))
SessionAutostartMiddleware.applyInitialMiddleware(webRouter) SessionAutostartMiddleware.applyInitialMiddleware(webRouter)
Modules.registerMiddleware(webRouter, 'sessionMiddleware', { Modules.registerMiddleware(webRouter, 'sessionMiddleware', {
store: sessionStore, store: sessionStore,
@ -167,7 +173,7 @@ webRouter.use(
session({ session({
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
secret: Settings.security.sessionSecret, secret: sessionSecrets,
proxy: Settings.behindProxy, proxy: Settings.behindProxy,
cookie: { cookie: {
domain: Settings.cookieDomain, domain: Settings.cookieDomain,

View file

@ -43,8 +43,6 @@ if (httpAuthUser && httpAuthPass) {
httpAuthUsers[httpAuthUser] = httpAuthPass httpAuthUsers[httpAuthUser] = httpAuthPass
} }
const sessionSecret = process.env.SESSION_SECRET
const intFromEnv = function (name, defaultValue) { const intFromEnv = function (name, defaultValue) {
if ( if (
[null, undefined].includes(defaultValue) || [null, undefined].includes(defaultValue) ||
@ -386,7 +384,9 @@ module.exports = {
// Security // Security
// -------- // --------
security: { security: {
sessionSecret, sessionSecret: process.env.SESSION_SECRET,
sessionSecretUpcoming: process.env.SESSION_SECRET_UPCOMING,
sessionSecretFallback: process.env.SESSION_SECRET_FALLBACK,
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12, bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12,
}, // number of rounds used to hash user passwords (raised to power 2) }, // number of rounds used to hash user passwords (raised to power 2)

View file

@ -100,7 +100,7 @@
"content-disposition": "^0.5.0", "content-disposition": "^0.5.0",
"contentful": "^10.8.5", "contentful": "^10.8.5",
"cookie": "^0.2.3", "cookie": "^0.2.3",
"cookie-parser": "1.3.5", "cookie-parser": "1.4.6",
"core-js": "^3.30.2", "core-js": "^3.30.2",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"csurf": "^1.11.0", "csurf": "^1.11.0",

View file

@ -17,6 +17,8 @@ module.exports = {
secureCookie: false, secureCookie: false,
security: { security: {
sessionSecret: 'static-secret-for-tests', sessionSecret: 'static-secret-for-tests',
sessionSecretUpcoming: 'static-secret-upcoming-for-tests',
sessionSecretFallback: 'static-secret-fallback-for-tests',
}, },
adminDomains: process.env.ADMIN_DOMAINS adminDomains: process.env.ADMIN_DOMAINS
? JSON.parse(process.env.ADMIN_DOMAINS) ? JSON.parse(process.env.ADMIN_DOMAINS)