Skip to content

Revert "NFDIV-4607 csrf token without csurf" #4214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"config": "3.3.12",
"connect-redis": "7.1.1",
"cookie-parser": "1.4.7",
"csurf": "1.11.0",
"dayjs": "1.11.13",
"email-validator": "2.0.4",
"express": "^4.21.2",
Expand Down
59 changes: 12 additions & 47 deletions src/main/modules/csrf/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,25 @@
import crypto from 'crypto';

import { NextFunction, Request, RequestHandler, Response } from 'express';
import { LoggerInstance } from 'winston';
import csurf from 'csurf';
import type { Application, RequestHandler } from 'express';
import type { LoggerInstance } from 'winston';

import { CSRF_TOKEN_ERROR_URL } from '../../steps/urls';

const { Logger } = require('@hmcts/nodejs-logging');
const logger: LoggerInstance = Logger.getLogger('app');

// Extend Express Request to include csrfToken
declare module 'express-session' {
interface SessionData {
csrfToken?: string;
}
}

declare module 'express' {
interface Request {
csrfToken?: () => string;
}
}

export class CSRFToken {
public enableFor(): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.session) {
return next(new Error('Session middleware is required before CSRF protection'));
}

// Store CSRF token in session to persist across requests
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}

// Expose the CSRF token as a function (to match expected type)
req.csrfToken = () => req.session.csrfToken as string;

// Attach CSRF token to response locals and headers
public enableFor(app: Application): void {
app.use(csurf() as unknown as RequestHandler, (req, res, next) => {
res.locals.csrfToken = req.csrfToken();
res.setHeader('x-csrf-token', req.csrfToken());

// Validate CSRF token for state-changing requests
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const csrfHeader = req.headers['x-csrf-token'];
const csrfBody = req.body?._csrf;
const csrfQuery = req.query?._csrf;
const csrfSessionToken = req.session.csrfToken;
const validToken = [csrfHeader, csrfBody, csrfQuery].includes(csrfSessionToken);
next();
});

if (!csrfSessionToken || !validToken) {
logger.debug(`CSRF validation failed for ${req.method} ${req.originalUrl}`);
return res.redirect(CSRF_TOKEN_ERROR_URL);
}
app.use((error, req, res, next) => {
if (error.code === 'EBADCSRFTOKEN') {
logger.debug(`${error.stack || error}`);
return res.redirect(CSRF_TOKEN_ERROR_URL);
}

next();
};
});
}
}
5 changes: 2 additions & 3 deletions src/main/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const logger: LoggerInstance = Logger.getLogger('server');
const app = express();

app.locals.developmentMode = process.env.NODE_ENV !== 'production';
app.use(favicon(path.join(__dirname, '/public/assets/images/favicon.ico')) as RequestHandler);
app.use(favicon(path.join(__dirname, '/public/assets/images/favicon.ico')));
app.use(express.static(path.join(__dirname, 'public')));
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
Expand Down Expand Up @@ -60,8 +60,7 @@ app.use((req, res, next) => {
app.use(bodyParser.json() as RequestHandler);
app.use(bodyParser.urlencoded({ extended: false }) as RequestHandler);

new SessionStorage().enableFor(app, logger);
app.use(new CSRFToken().enableFor());
new CSRFToken().enableFor(app);
new LanguageToggle().enableFor(app);
new AuthProvider().enable();
new FeesRegister().enable();
Expand Down
83 changes: 81 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6883,6 +6883,13 @@ __metadata:
languageName: node
linkType: hard

"cookie@npm:0.4.0":
version: 0.4.0
resolution: "cookie@npm:0.4.0"
checksum: 760384ba0aef329c52523747e36a452b5e51bc49b34160363a6934e7b7df3f93fcc88b35e33450361535d40a92a96412da870e1816aba9aa6cc556a9fedd8492
languageName: node
linkType: hard

"cookie@npm:0.7.1":
version: 0.7.1
resolution: "cookie@npm:0.7.1"
Expand Down Expand Up @@ -7055,6 +7062,17 @@ __metadata:
languageName: node
linkType: hard

"csrf@npm:3.1.0":
version: 3.1.0
resolution: "csrf@npm:3.1.0"
dependencies:
rndm: 1.2.0
tsscmp: 1.0.6
uid-safe: 2.1.5
checksum: bb151ccd76203bf4d443d01c63a9122cdcc3e7c6c6213518d836c440a4a9fc05543078086e45add14c0c1e1d57592068967b8ec9cc1fa67c01a49474c988c4e9
languageName: node
linkType: hard

"css-loader@npm:7.1.2":
version: 7.1.2
resolution: "css-loader@npm:7.1.2"
Expand Down Expand Up @@ -7161,6 +7179,18 @@ __metadata:
languageName: node
linkType: hard

"csurf@npm:1.11.0":
version: 1.11.0
resolution: "csurf@npm:1.11.0"
dependencies:
cookie: 0.4.0
cookie-signature: 1.0.6
csrf: 3.1.0
http-errors: ~1.7.3
checksum: c41a1ec593000301f8ad57b8b43c13520c9939af61e0ed2d03f753f69870478ba40f874c02349312ff001c2d349421b649aef4af222b3c699bb10f1d98ce2860
languageName: node
linkType: hard

"cycle@npm:1.0.x":
version: 1.0.3
resolution: "cycle@npm:1.0.3"
Expand Down Expand Up @@ -7463,6 +7493,13 @@ __metadata:
languageName: node
linkType: hard

"depd@npm:~1.1.2":
version: 1.1.2
resolution: "depd@npm:1.1.2"
checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9
languageName: node
linkType: hard

"deprecated-react-native-prop-types@npm:^5.0.0":
version: 5.0.0
resolution: "deprecated-react-native-prop-types@npm:5.0.0"
Expand Down Expand Up @@ -10207,6 +10244,19 @@ __metadata:
languageName: node
linkType: hard

"http-errors@npm:~1.7.3":
version: 1.7.3
resolution: "http-errors@npm:1.7.3"
dependencies:
depd: ~1.1.2
inherits: 2.0.4
setprototypeof: 1.1.1
statuses: ">= 1.5.0 < 2"
toidentifier: 1.0.0
checksum: a59f359473f4b3ea78305beee90d186268d6075432622a46fb7483059068a2dd4c854a20ac8cd438883127e06afb78c1309168bde6cdfeed1e3700eb42487d99
languageName: node
linkType: hard

"http-proxy-agent@npm:^4.0.1":
version: 4.0.1
resolution: "http-proxy-agent@npm:4.0.1"
Expand Down Expand Up @@ -13837,6 +13887,7 @@ __metadata:
cookie-parser: 1.4.7
copy-webpack-plugin: 13.0.0
css-loader: 7.1.2
csurf: 1.11.0
dayjs: 1.11.13
email-validator: 2.0.4
eslint: 8.57.1
Expand Down Expand Up @@ -16303,6 +16354,13 @@ __metadata:
languageName: node
linkType: hard

"rndm@npm:1.2.0":
version: 1.2.0
resolution: "rndm@npm:1.2.0"
checksum: ff0c54a12357267108d7de17b762869e388215e2e3c8147b5bba80d8454ee490c5fdaa40020b8b52ca52b86f13ad6171b4845dd5b7f627e5e2b6195908117c07
languageName: node
linkType: hard

"run-async@npm:^2.2.0":
version: 2.4.1
resolution: "run-async@npm:2.4.1"
Expand Down Expand Up @@ -16689,6 +16747,13 @@ __metadata:
languageName: node
linkType: hard

"setprototypeof@npm:1.1.1":
version: 1.1.1
resolution: "setprototypeof@npm:1.1.1"
checksum: a8bee29c1c64c245d460ce53f7460af8cbd0aceac68d66e5215153992cc8b3a7a123416353e0c642060e85cc5fd4241c92d1190eec97eda0dcb97436e8fcca3b
languageName: node
linkType: hard

"setprototypeof@npm:1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
Expand Down Expand Up @@ -17126,7 +17191,7 @@ __metadata:
languageName: node
linkType: hard

"statuses@npm:~1.5.0":
"statuses@npm:>= 1.5.0 < 2, statuses@npm:~1.5.0":
version: 1.5.0
resolution: "statuses@npm:1.5.0"
checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c
Expand Down Expand Up @@ -17800,6 +17865,13 @@ __metadata:
languageName: node
linkType: hard

"toidentifier@npm:1.0.0":
version: 1.0.0
resolution: "toidentifier@npm:1.0.0"
checksum: 199e6bfca1531d49b3506cff02353d53ec987c9ee10ee272ca6484ed97f1fc10fb77c6c009079ca16d5c5be4a10378178c3cacdb41ce9ec954c3297c74c6053e
languageName: node
linkType: hard

"toidentifier@npm:1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
Expand Down Expand Up @@ -18025,6 +18097,13 @@ __metadata:
languageName: node
linkType: hard

"tsscmp@npm:1.0.6":
version: 1.0.6
resolution: "tsscmp@npm:1.0.6"
checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf
languageName: node
linkType: hard

"tsx@npm:^4.7.2":
version: 4.19.2
resolution: "tsx@npm:4.19.2"
Expand Down Expand Up @@ -18265,7 +18344,7 @@ __metadata:
languageName: node
linkType: hard

"uid-safe@npm:~2.1.5":
"uid-safe@npm:2.1.5, uid-safe@npm:~2.1.5":
version: 2.1.5
resolution: "uid-safe@npm:2.1.5"
dependencies:
Expand Down