Merge pull request #3351 from SillyTavern/csrf-sync Replace CSRF middleware

c9a1a3eb94bdc0b823b115e8e230380822d9e11e

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +53 -33Showing whitespace changes
default/config.yaml+1 -1
@@ -70,7 +70,7 @@ perUserBasicAuth: false
70## Set to a positive number to expire session after a certain time of inactivity70## 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 closed71## Set to 0 to expire session when the browser is closed
72## Set to a negative number to disable session expiration72## Set to a negative number to disable session expiration
73sessionTimeout: 8640073sessionTimeout: -1
74# Used to sign session cookies. Will be auto-generated if not set74# Used to sign session cookies. Will be auto-generated if not set
75cookieSecret: ''75cookieSecret: ''
76# Disable CSRF protection - NOT RECOMMENDED76# Disable CSRF protection - NOT RECOMMENDED
index.d.ts+18 -8
@@ -1,6 +1,24 @@
1import { UserDirectoryList, User } from "./src/users";1import { UserDirectoryList, User } from "./src/users";
2import { CsrfSyncedToken } from "csrf-sync";
23
3declare global {4declare 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 namespace Express {22 namespace Express {
5 export interface Request {23 export interface Request {
6 user: {24 user: {
@@ -15,11 +33,3 @@ declare global {
15 */33 */
16 var DATA_ROOT: string;34 var DATA_ROOT: string;
17}35}
18
19declare module 'express-session' {
20 export interface SessionData {
21 handle: string;
22 touch: number;
23 // other properties...
24 }
25 }
package-lock.json+5 -5
@@ -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"
package.json+1 -1
@@ -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",
server.js+27 -18
@@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers';
1818
19// express/server related library imports19// express/server related library imports
20import cors from 'cors';20import cors from 'cors';
21import { doubleCsrf } from 'csrf-csrf';21import { csrfSync } from 'csrf-sync';
22import express from 'express';22import express from 'express';
23import compression from 'compression';23import compression from 'compression';
24import cookieParser from 'cookie-parser';
25import cookieSession from 'cookie-session';24import cookieSession from 'cookie-session';
26import multer from 'multer';25import multer from 'multer';
27import responseTime from 'response-time';26import responseTime from 'response-time';
@@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4;
40import { loadPlugins } from './src/plugin-loader.js';39import { loadPlugins } from './src/plugin-loader.js';
41import {40import {
42 initUserStorage,41 initUserStorage,
43 getCsrfSecret,
44 getCookieSecret,42 getCookieSecret,
45 getCookieSessionName,43 getCookieSessionName,
46 getAllEnabledUsers,44 getAllEnabledUsers,
@@ -348,8 +346,8 @@ if (enableCorsProxy) {
348}346}
349347
350function getSessionCookieAge() {348function getSessionCookieAge() {
351 // Defaults to 24 hours in seconds if not set349 // Defaults to "no expiration" if not set
352 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60);350 const configValue = getConfigValue('sessionTimeout', -1);
353351
354 // Convert to milliseconds352 // Convert to milliseconds
355 if (configValue > 0) {353 if (configValue > 0) {
@@ -378,27 +376,38 @@ app.use(setUserDataMiddleware);
378376
379// CSRF Protection //377// CSRF Protection //
380if (!disableCsrf) {378if (!disableCsrf) {
381 const COOKIES_SECRET = getCookieSecret();379 const csrfSyncProtection = csrfSync({
382380 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,386 },
387 getTokenFromRequest: (req) => {
388 return req.headers['x-csrf-token']?.toString();
389 },389 },
390 size: 64,390 storeTokenInState: (req, token) => {
391 getTokenFromRequest: (req) => req.headers['x-csrf-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 });
393399
394 app.get('/csrf-token', (req, res) => {400 app.get('/csrf-token', (req, res) => {
395 res.json({401 res.json({
396 'token': generateToken(res, req),402 'token': csrfSyncProtection.generateToken(req),
397 });403 });
398 });404 });
399405
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} else {411} else {
403 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');412 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
404 app.get('/csrf-token', (req, res) => {413 app.get('/csrf-token', (req, res) => {
src/endpoints/users-private.js+1 -0
@@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => {
23 }23 }
2424
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) {