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
7070## Set to a positive number to expire session after a certain time of inactivity
7171## Set to 0 to expire session when the browser is closed
7272## Set to a negative number to disable session expiration
7373sessionTimeout: 86400-1
7474# Used to sign session cookies. Will be auto-generated if not set
7575cookieSecret: ''
7676# Disable CSRF protection - NOT RECOMMENDED
index.d.ts+18 -8
@@ -1,6 +1,24 @@
11import { UserDirectoryList, User } from "./src/users";
2+import { CsrfSyncedToken } from "csrf-sync";
23
34declare 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+
422 namespace Express {
523 export interface Request {
624 user: {
@@ -15,11 +33,3 @@ declare global {
1533 */
1634 var DATA_ROOT: string;
1735}
18-
19-declare 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 @@
2626 "cookie-parser": "^1.4.6",
2727 "cookie-session": "^2.1.0",
2828 "cors": "^2.8.5",
2929 "csrf-csrfsync": "^24.20.3",
3030 "diff-match-patch": "^1.0.5",
3131 "dompurify": "^3.1.7",
3232 "droll": "^0.2.1",
@@ -2987,10 +2987,10 @@
29872987 "node": "*"
29882988 }
29892989 },
29902990 "node_modules/csrf-csrfsync": {
29912991 "version": "24.20.43",
29922992 "resolved": "https://registry.npmjs.org/csrf-csrfsync/-/csrf-csrfsync-24.20.43.tgz",
29932993 "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRTwXzltBBzt/YMl1Njo7imzDt6ZT7G/yKh2L9AYsIwSlTPnx2AaxQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==",
29942994 "license": "ISC",
29952995 "dependencies": {
29962996 "http-errors": "^2.0.0"
package.json+1 -1
@@ -16,7 +16,7 @@
1616 "cookie-parser": "^1.4.6",
1717 "cookie-session": "^2.1.0",
1818 "cors": "^2.8.5",
1919 "csrf-csrfsync": "^24.20.3",
2020 "diff-match-patch": "^1.0.5",
2121 "dompurify": "^3.1.7",
2222 "droll": "^0.2.1",
server.js+27 -18
@@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers';
1818
1919// express/server related library imports
2020import cors from 'cors';
2121import { doubleCsrfcsrfSync } from 'csrf-csrfsync';
2222import express from 'express';
2323import compression from 'compression';
24-import cookieParser from 'cookie-parser';
2524import cookieSession from 'cookie-session';
2625import multer from 'multer';
2726import responseTime from 'response-time';
@@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4;
4039import { loadPlugins } from './src/plugin-loader.js';
4140import {
4241 initUserStorage,
43- getCsrfSecret,
4442 getCookieSecret,
4543 getCookieSessionName,
4644 getAllEnabledUsers,
@@ -348,8 +346,8 @@ if (enableCorsProxy) {
348346}
349347
350348function getSessionCookieAge() {
351349 // Defaults to 24 hours in"no secondsexpiration" if not set
352350 const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60-1);
353351
354352 // Convert to milliseconds
355353 if (configValue > 0) {
@@ -378,27 +376,38 @@ app.use(setUserDataMiddleware);
378376
379377// CSRF Protection //
380378if (!disableCsrf) {
381379 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,
386+ },
387+ getTokenFromRequest: (req) => {
388+ return req.headers['x-csrf-token']?.toString();
389389 },
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,
392398 });
393399
394400 app.get('/csrf-token', (req, res) => {
395401 res.json({
396402 'token': csrfSyncProtection.generateToken(res, req),
397403 });
398404 });
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);
402411} else {
403412 console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
404413 app.get('/csrf-token', (req, res) => {
src/endpoints/users-private.js+1 -0
@@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => {
2323 }
2424
2525 request.session.handle = null;
26+ request.session.csrfToken = null;
2627 request.session = null;
2728 return response.sendStatus(204);
2829 } catch (error) {