Merge pull request #3351 from SillyTavern/csrf-sync Replace CSRF middleware
Signed| @@ -70,7 +70,7 @@ perUserBasicAuth: false | ||
| 70 | 70 | ## Set to a positive number to expire session after a certain time of inactivity |
| 71 | 71 | ## Set to 0 to expire session when the browser is closed |
| 72 | 72 | ## Set to a negative number to disable session expiration |
| 73 | 73 | sessionTimeout: 86400-1 |
| 74 | 74 | # Used to sign session cookies. Will be auto-generated if not set |
| 75 | 75 | cookieSecret: '' |
| 76 | 76 | # Disable CSRF protection - NOT RECOMMENDED |
| @@ -1,6 +1,24 @@ | ||
| 1 | 1 | import { UserDirectoryList, User } from "./src/users"; |
| 2 | +import { CsrfSyncedToken } from "csrf-sync"; | |
| 2 | 3 | |
| 3 | 4 | declare global { |
| 5 | + declare namespace CookieSessionInterfaces { | |
| 6 | + export interface CookieSessionObject { | |
| 7 | + /** | |
| 8 | + * The CSRF token for the session. | |
| 9 | + */ | |
| 10 | + csrfToken: CsrfSyncedToken; | |
| 11 | + /** | |
| 12 | + * Authenticated user handle. | |
| 13 | + */ | |
| 14 | + handle: string; | |
| 15 | + /** | |
| 16 | + * Last time the session was extended. | |
| 17 | + */ | |
| 18 | + touch: number; | |
| 19 | + } | |
| 20 | + } | |
| 21 | + | |
| 4 | 22 | namespace Express { |
| 5 | 23 | export interface Request { |
| 6 | 24 | user: { |
| @@ -15,11 +33,3 @@ declare global { | ||
| 15 | 33 | */ |
| 16 | 34 | var DATA_ROOT: string; |
| 17 | 35 | } |
| 18 | - | |
| 19 | -declare module 'express-session' { | |
| 20 | - export interface SessionData { | |
| 21 | - handle: string; | |
| 22 | - touch: number; | |
| 23 | - // other properties... | |
| 24 | - } | |
| 25 | - } | |
| @@ -26,7 +26,7 @@ | ||
| 26 | 26 | "cookie-parser": "^1.4.6", |
| 27 | 27 | "cookie-session": "^2.1.0", |
| 28 | 28 | "cors": "^2.8.5", |
| 29 | 29 | "csrf-csrfsync": "^24.20.3", |
| 30 | 30 | "diff-match-patch": "^1.0.5", |
| 31 | 31 | "dompurify": "^3.1.7", |
| 32 | 32 | "droll": "^0.2.1", |
| @@ -2987,10 +2987,10 @@ | ||
| 2987 | 2987 | "node": "*" |
| 2988 | 2988 | } |
| 2989 | 2989 | }, |
| 2990 | 2990 | "node_modules/csrf-csrfsync": { |
| 2991 | 2991 | "version": "24.20.43", |
| 2992 | 2992 | "resolved": "https://registry.npmjs.org/csrf-csrfsync/-/csrf-csrfsync-24.20.43.tgz", |
| 2993 | 2993 | "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRTwXzltBBzt/YMl1Njo7imzDt6ZT7G/yKh2L9AYsIwSlTPnx2AaxQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==", |
| 2994 | 2994 | "license": "ISC", |
| 2995 | 2995 | "dependencies": { |
| 2996 | 2996 | "http-errors": "^2.0.0" |
| @@ -16,7 +16,7 @@ | ||
| 16 | 16 | "cookie-parser": "^1.4.6", |
| 17 | 17 | "cookie-session": "^2.1.0", |
| 18 | 18 | "cors": "^2.8.5", |
| 19 | 19 | "csrf-csrfsync": "^24.20.3", |
| 20 | 20 | "diff-match-patch": "^1.0.5", |
| 21 | 21 | "dompurify": "^3.1.7", |
| 22 | 22 | "droll": "^0.2.1", |
| @@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers'; | ||
| 18 | 18 | |
| 19 | 19 | // express/server related library imports |
| 20 | 20 | import cors from 'cors'; |
| 21 | 21 | import { doubleCsrfcsrfSync } from 'csrf-csrfsync'; |
| 22 | 22 | import express from 'express'; |
| 23 | 23 | import compression from 'compression'; |
| 24 | -import cookieParser from 'cookie-parser'; | |
| 25 | 24 | import cookieSession from 'cookie-session'; |
| 26 | 25 | import multer from 'multer'; |
| 27 | 26 | import responseTime from 'response-time'; |
| @@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4; | ||
| 40 | 39 | import { loadPlugins } from './src/plugin-loader.js'; |
| 41 | 40 | import { |
| 42 | 41 | initUserStorage, |
| 43 | - getCsrfSecret, | |
| 44 | 42 | getCookieSecret, |
| 45 | 43 | getCookieSessionName, |
| 46 | 44 | getAllEnabledUsers, |
| @@ -348,8 +346,8 @@ if (enableCorsProxy) { | ||
| 348 | 346 | } |
| 349 | 347 | |
| 350 | 348 | function getSessionCookieAge() { |
| 351 | 349 | // Defaults to 24 hours in"no secondsexpiration" if not set |
| 352 | 350 | const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60-1); |
| 353 | 351 | |
| 354 | 352 | // Convert to milliseconds |
| 355 | 353 | if (configValue > 0) { |
| @@ -378,27 +376,38 @@ app.use(setUserDataMiddleware); | ||
| 378 | 376 | |
| 379 | 377 | // CSRF Protection // |
| 380 | 378 | if (!disableCsrf) { |
| 381 | 379 | const COOKIES_SECRETcsrfSyncProtection = getCookieSecretcsrfSync();{ |
| 382 | - | |
| 380 | + getTokenFromState: (req) => { | |
| 383 | - const { generateToken, doubleCsrfProtection } = doubleCsrf({ | |
| 381 | + if (!req.session) { | |
| 384 | - getSecret: getCsrfSecret, | |
| 382 | + console.error('(CSRF error) getTokenFromState: Session object not initialized'); | |
| 385 | - cookieName: 'X-CSRF-Token', | |
| 383 | + return; | |
| 386 | - cookieOptions: { | |
| 384 | + } | |
| 387 | - sameSite: 'strict', | |
| 385 | + return req.session.csrfToken; | |
| 388 | - secure: false, | |
| 389 | 386 | }, |
| 390 | - size: 64, | |
| 387 | + getTokenFromRequest: (req) => { | |
| 391 | 388 | getTokenFromRequest: (req) => return req.headers['x-csrf-token'],?.toString(); |
| 389 | + }, | |
| 390 | + storeTokenInState: (req, token) => { | |
| 391 | + if (!req.session) { | |
| 392 | + console.error('(CSRF error) storeTokenInState: Session object not initialized'); | |
| 393 | + return; | |
| 394 | + } | |
| 395 | + req.session.csrfToken = token; | |
| 396 | + }, | |
| 397 | + size: 32, | |
| 392 | 398 | }); |
| 393 | 399 | |
| 394 | 400 | app.get('/csrf-token', (req, res) => { |
| 395 | 401 | res.json({ |
| 396 | 402 | 'token': csrfSyncProtection.generateToken(res, req), |
| 397 | 403 | }); |
| 398 | 404 | }); |
| 399 | 405 | |
| 400 | - app.use(cookieParser(COOKIES_SECRET)); | |
| 406 | + // Customize the error message | |
| 401 | - app.use(doubleCsrfProtection); | |
| 407 | + csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); | |
| 408 | + csrfSyncProtection.invalidCsrfTokenError.stack = undefined; | |
| 409 | + | |
| 410 | + app.use(csrfSyncProtection.csrfSynchronisedProtection); | |
| 402 | 411 | } else { |
| 403 | 412 | console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); |
| 404 | 413 | app.get('/csrf-token', (req, res) => { |
| @@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => { | ||
| 23 | 23 | } |
| 24 | 24 | |
| 25 | 25 | request.session.handle = null; |
| 26 | + request.session.csrfToken = null; | |
| 26 | 27 | request.session = null; |
| 27 | 28 | return response.sendStatus(204); |
| 28 | 29 | } catch (error) { |