Replace CSRF middleware Closes #3349
| @@ -70,7 +70,7 @@ perUserBasicAuth: false | |||
| 70 | ## Set to a positive number to expire session after a certain time of inactivity | 70 | ## Set to a positive number to expire session after a certain time of inactivity |
| 71 | ## Set to 0 to expire session when the browser is closed | 71 | ## Set to 0 to expire session when the browser is closed |
| 72 | ## Set to a negative number to disable session expiration | 72 | ## Set to a negative number to disable session expiration |
| 73 | sessionTimeout: 86400 | 73 | sessionTimeout: -1 |
| 74 | # Used to sign session cookies. Will be auto-generated if not set | 74 | # Used to sign session cookies. Will be auto-generated if not set |
| 75 | cookieSecret: '' | 75 | cookieSecret: '' |
| 76 | # Disable CSRF protection - NOT RECOMMENDED | 76 | # Disable CSRF protection - NOT RECOMMENDED |
| @@ -26,7 +26,7 @@ | |||
| 26 | "cookie-parser": "^1.4.6", | 26 | "cookie-parser": "^1.4.6", |
| 27 | "cookie-session": "^2.1.0", | 27 | "cookie-session": "^2.1.0", |
| 28 | "cors": "^2.8.5", | 28 | "cors": "^2.8.5", |
| 29 | "csrf-csrf": "^2.2.3", | 29 | "csrf-sync": "^4.0.3", |
| 30 | "diff-match-patch": "^1.0.5", | 30 | "diff-match-patch": "^1.0.5", |
| 31 | "dompurify": "^3.1.7", | 31 | "dompurify": "^3.1.7", |
| 32 | "droll": "^0.2.1", | 32 | "droll": "^0.2.1", |
| @@ -2987,10 +2987,10 @@ | |||
| 2987 | "node": "*" | 2987 | "node": "*" |
| 2988 | } | 2988 | } |
| 2989 | }, | 2989 | }, |
| 2990 | "node_modules/csrf-csrf": { | 2990 | "node_modules/csrf-sync": { |
| 2991 | "version": "2.2.4", | 2991 | "version": "4.0.3", |
| 2992 | "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz", | 2992 | "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.0.3.tgz", |
| 2993 | "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==", | 2993 | "integrity": "sha512-wXzltBBzt/7imzDt6ZT7G/axQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==", |
| 2994 | "license": "ISC", | 2994 | "license": "ISC", |
| 2995 | "dependencies": { | 2995 | "dependencies": { |
| 2996 | "http-errors": "^2.0.0" | 2996 | "http-errors": "^2.0.0" |
| @@ -16,7 +16,7 @@ | |||
| 16 | "cookie-parser": "^1.4.6", | 16 | "cookie-parser": "^1.4.6", |
| 17 | "cookie-session": "^2.1.0", | 17 | "cookie-session": "^2.1.0", |
| 18 | "cors": "^2.8.5", | 18 | "cors": "^2.8.5", |
| 19 | "csrf-csrf": "^2.2.3", | 19 | "csrf-sync": "^4.0.3", |
| 20 | "diff-match-patch": "^1.0.5", | 20 | "diff-match-patch": "^1.0.5", |
| 21 | "dompurify": "^3.1.7", | 21 | "dompurify": "^3.1.7", |
| 22 | "droll": "^0.2.1", | 22 | "droll": "^0.2.1", |
| @@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers'; | |||
| 18 | 18 | ||
| 19 | // express/server related library imports | 19 | // express/server related library imports |
| 20 | import cors from 'cors'; | 20 | import cors from 'cors'; |
| 21 | import { doubleCsrf } from 'csrf-csrf'; | 21 | import { csrfSync } from 'csrf-sync'; |
| 22 | import express from 'express'; | 22 | import express from 'express'; |
| 23 | import compression from 'compression'; | 23 | import compression from 'compression'; |
| 24 | import cookieParser from 'cookie-parser'; | ||
| 25 | import cookieSession from 'cookie-session'; | 24 | import cookieSession from 'cookie-session'; |
| 26 | import multer from 'multer'; | 25 | import multer from 'multer'; |
| 27 | import responseTime from 'response-time'; | 26 | import responseTime from 'response-time'; |
| @@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4; | |||
| 40 | import { loadPlugins } from './src/plugin-loader.js'; | 39 | import { loadPlugins } from './src/plugin-loader.js'; |
| 41 | import { | 40 | import { |
| 42 | initUserStorage, | 41 | initUserStorage, |
| 43 | getCsrfSecret, | ||
| 44 | getCookieSecret, | 42 | getCookieSecret, |
| 45 | getCookieSessionName, | 43 | getCookieSessionName, |
| 46 | getAllEnabledUsers, | 44 | getAllEnabledUsers, |
| @@ -347,8 +345,8 @@ if (enableCorsProxy) { | |||
| 347 | } | 345 | } |
| 348 | 346 | ||
| 349 | function getSessionCookieAge() { | 347 | function getSessionCookieAge() { |
| 350 | // Defaults to 24 hours in seconds if not set | 348 | // Defaults to "no expiration" if not set |
| 351 | const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60); | 349 | const configValue = getConfigValue('sessionTimeout', -1); |
| 352 | 350 | ||
| 353 | // Convert to milliseconds | 351 | // Convert to milliseconds |
| 354 | if (configValue > 0) { | 352 | if (configValue > 0) { |
| @@ -377,27 +375,34 @@ app.use(setUserDataMiddleware); | |||
| 377 | 375 | ||
| 378 | // CSRF Protection // | 376 | // CSRF Protection // |
| 379 | if (!disableCsrf) { | 377 | if (!disableCsrf) { |
| 380 | const COOKIES_SECRET = getCookieSecret(); | 378 | const csrfSyncProtection = csrfSync({ |
| 381 | 379 | getTokenFromState: (req) => { | |
| 382 | const { generateToken, doubleCsrfProtection } = doubleCsrf({ | 380 | if (!req.session) { |
| 383 | getSecret: getCsrfSecret, | 381 | console.error('(CSRF error) getTokenFromState: Session object not initialized'); |
| 384 | cookieName: 'X-CSRF-Token', | 382 | return; |
| 385 | cookieOptions: { | 383 | } |
| 386 | sameSite: 'strict', | 384 | return req.session.csrfToken; |
| 387 | secure: false, | 385 | }, |
| 386 | getTokenFromRequest: (req) => { | ||
| 387 | return req.headers['x-csrf-token']?.toString(); | ||
| 388 | }, | ||
| 389 | storeTokenInState: (req, token) => { | ||
| 390 | if (!req.session) { | ||
| 391 | console.error('(CSRF error) storeTokenInState: Session object not initialized'); | ||
| 392 | return; | ||
| 393 | } | ||
| 394 | req.session.csrfToken = token; | ||
| 388 | }, | 395 | }, |
| 389 | size: 64, | 396 | size: 32, |
| 390 | getTokenFromRequest: (req) => req.headers['x-csrf-token'], | ||
| 391 | }); | 397 | }); |
| 392 | 398 | ||
| 393 | app.get('/csrf-token', (req, res) => { | 399 | app.get('/csrf-token', (req, res) => { |
| 394 | res.json({ | 400 | res.json({ |
| 395 | 'token': generateToken(res, req), | 401 | 'token': csrfSyncProtection.generateToken(req), |
| 396 | }); | 402 | }); |
| 397 | }); | 403 | }); |
| 398 | 404 | ||
| 399 | app.use(cookieParser(COOKIES_SECRET)); | 405 | app.use(csrfSyncProtection.csrfSynchronisedProtection); |
| 400 | app.use(doubleCsrfProtection); | ||
| 401 | } else { | 406 | } else { |
| 402 | console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); | 407 | console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); |
| 403 | app.get('/csrf-token', (req, res) => { | 408 | app.get('/csrf-token', (req, res) => { |
| @@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => { | |||
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | request.session.handle = null; | 25 | request.session.handle = null; |
| 26 | request.session.csrfToken = null; | ||
| 26 | request.session = null; | 27 | request.session = null; |
| 27 | return response.sendStatus(204); | 28 | return response.sendStatus(204); |
| 28 | } catch (error) { | 29 | } catch (error) { |