diff --git a/default/config.yaml b/default/config.yaml index 4c28044dc..f3e7db625 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -70,7 +70,7 @@ perUserBasicAuth: false ## Set to a positive number to expire session after a certain time of inactivity ## Set to 0 to expire session when the browser is closed ## Set to a negative number to disable session expiration -sessionTimeout: 86400 +sessionTimeout: -1 # Used to sign session cookies. Will be auto-generated if not set cookieSecret: '' # Disable CSRF protection - NOT RECOMMENDED diff --git a/index.d.ts b/index.d.ts index 015d8e353..35f34c22c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,24 @@ import { UserDirectoryList, User } from "./src/users"; +import { CsrfSyncedToken } from "csrf-sync"; declare global { + declare namespace CookieSessionInterfaces { + export interface CookieSessionObject { + /** + * The CSRF token for the session. + */ + csrfToken: CsrfSyncedToken; + /** + * Authenticated user handle. + */ + handle: string; + /** + * Last time the session was extended. + */ + touch: number; + } + } + namespace Express { export interface Request { user: { @@ -15,11 +33,3 @@ declare global { */ var DATA_ROOT: string; } - -declare module 'express-session' { - export interface SessionData { - handle: string; - touch: number; - // other properties... - } - } diff --git a/package-lock.json b/package-lock.json index 5784a9946..4e79f9f50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", "cors": "^2.8.5", - "csrf-csrf": "^2.2.3", + "csrf-sync": "^4.0.3", "diff-match-patch": "^1.0.5", "dompurify": "^3.1.7", "droll": "^0.2.1", @@ -2987,10 +2987,10 @@ "node": "*" } }, - "node_modules/csrf-csrf": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz", - "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==", + "node_modules/csrf-sync": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.0.3.tgz", + "integrity": "sha512-wXzltBBzt/7imzDt6ZT7G/axQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==", "license": "ISC", "dependencies": { "http-errors": "^2.0.0" diff --git a/package.json b/package.json index 672ffada5..b14a487b8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", "cors": "^2.8.5", - "csrf-csrf": "^2.2.3", + "csrf-sync": "^4.0.3", "diff-match-patch": "^1.0.5", "dompurify": "^3.1.7", "droll": "^0.2.1", diff --git a/server.js b/server.js index cbd161847..6cd78d753 100644 --- a/server.js +++ b/server.js @@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers'; // express/server related library imports import cors from 'cors'; -import { doubleCsrf } from 'csrf-csrf'; +import { csrfSync } from 'csrf-sync'; import express from 'express'; import compression from 'compression'; -import cookieParser from 'cookie-parser'; import cookieSession from 'cookie-session'; import multer from 'multer'; import responseTime from 'response-time'; @@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4; import { loadPlugins } from './src/plugin-loader.js'; import { initUserStorage, - getCsrfSecret, getCookieSecret, getCookieSessionName, getAllEnabledUsers, @@ -348,8 +346,8 @@ if (enableCorsProxy) { } function getSessionCookieAge() { - // Defaults to 24 hours in seconds if not set - const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60); + // Defaults to "no expiration" if not set + const configValue = getConfigValue('sessionTimeout', -1); // Convert to milliseconds if (configValue > 0) { @@ -378,27 +376,38 @@ app.use(setUserDataMiddleware); // CSRF Protection // if (!disableCsrf) { - const COOKIES_SECRET = getCookieSecret(); - - const { generateToken, doubleCsrfProtection } = doubleCsrf({ - getSecret: getCsrfSecret, - cookieName: 'X-CSRF-Token', - cookieOptions: { - sameSite: 'strict', - secure: false, + const csrfSyncProtection = csrfSync({ + getTokenFromState: (req) => { + if (!req.session) { + console.error('(CSRF error) getTokenFromState: Session object not initialized'); + return; + } + return req.session.csrfToken; }, - size: 64, - getTokenFromRequest: (req) => req.headers['x-csrf-token'], + getTokenFromRequest: (req) => { + return req.headers['x-csrf-token']?.toString(); + }, + storeTokenInState: (req, token) => { + if (!req.session) { + console.error('(CSRF error) storeTokenInState: Session object not initialized'); + return; + } + req.session.csrfToken = token; + }, + size: 32, }); app.get('/csrf-token', (req, res) => { res.json({ - 'token': generateToken(res, req), + 'token': csrfSyncProtection.generateToken(req), }); }); - app.use(cookieParser(COOKIES_SECRET)); - app.use(doubleCsrfProtection); + // Customize the error message + csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); + csrfSyncProtection.invalidCsrfTokenError.stack = undefined; + + app.use(csrfSyncProtection.csrfSynchronisedProtection); } else { console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); app.get('/csrf-token', (req, res) => { diff --git a/src/endpoints/users-private.js b/src/endpoints/users-private.js index 81b60acea..1fde87083 100644 --- a/src/endpoints/users-private.js +++ b/src/endpoints/users-private.js @@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => { } request.session.handle = null; + request.session.csrfToken = null; request.session = null; return response.sendStatus(204); } catch (error) {