From d249bfe640fa56bffe3e78798a29d5db341ff79c Mon Sep 17 00:00:00 2001
From: David
- You will need to be using cookie-parser and the middleware should be registered before Double CSRF. This utility will set a cookie containing a hash of the csrf token and provide the non-hashed csrf token so you can include it within your response. + You will need to be using cookie-parser and the middleware should be registered before Double CSRF. This utility will set a cookie containing both the csrf token and a hash of the csrf token and provide the non-hashed csrf token so you can include it within your response.
Requires TypeScript >= 3.8
@@ -98,7 +98,7 @@ const { doubleCsrf } = require("csrf-csrf"); ```js const { invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware. - generateToken, // Use this in your routes to provide a CSRF hash cookie and token. + generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token. validateRequest, // Also a convenience if you plan on making your own middleware. doubleCsrfProtection, // This is the default CSRF protection middleware. } = doubleCsrf(doubleCsrfOptions); diff --git a/src/index.ts b/src/index.ts index 97eaa78..d92b569 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,11 @@ export type RequestMethod = | "PATCH"; export type CsrfIgnoredMethods = RequestMethod[]; export type CsrfRequestValidator = (req: Request) => boolean; +export type CsrfTokenAndHashPairValidator = ( + token: string, + hash: string, + secret: string +) => boolean; export type CsrfCookieSetter = ( res: Response, name: string, @@ -92,6 +97,20 @@ export function doubleCsrf({ }); const generateTokenAndHash = (req: Request) => { + const csrfCookie = getCsrfCookieFromRequest(req); + // if csrfCookie is present, it means that there is already a session, so we extract + // the hash/token from it, validate it and reuse the token. This makes possible having + // multiple tabs open at the same time + if (typeof csrfCookie === "string") { + const [csrfToken, csrfTokenHash] = csrfCookie.split("|"); + const csrfSecret = getSecret(req); + if (!validateTokenAndHashPair(csrfToken, csrfTokenHash, csrfSecret)) { + // if the pair is not valid, then the cookie has been modified by a third party + throw invalidCsrfTokenError; + } + return { csrfToken, csrfTokenHash }; + } + // else, generate the token and hash from scratch const csrfToken = randomBytes(size).toString("hex"); const secret = getSecret(req); const csrfTokenHash = createHash("sha256") @@ -107,30 +126,48 @@ export function doubleCsrf({ // Do NOT send the csrfToken as a cookie, embed it in your HTML response, or as JSON. const generateToken = (res: Response, req: Request) => { const { csrfToken, csrfTokenHash } = generateTokenAndHash(req); - res.cookie(cookieName, csrfTokenHash, { ...cookieOptions, httpOnly: true }); + const cookieContent = `${csrfToken}|${csrfTokenHash}`; + res.cookie(cookieName, cookieContent, { ...cookieOptions, httpOnly: true }); return csrfToken; }; - const getTokenHashFromRequest = remainingCOokieOptions.signed + const getCsrfCookieFromRequest = remainingCOokieOptions.signed ? (req: Request) => req.signedCookies[cookieName] as string : (req: Request) => req.cookies[cookieName] as string; + // validates if a token and its hash matches, given the secret that was originally included in the hash + const validateTokenAndHashPair: CsrfTokenAndHashPairValidator = ( + token, + hash, + secret + ) => { + if (typeof token !== "string" || typeof hash !== "string") return false; + + const expectedHash = createHash("sha256") + .update(`${token}${secret}`) + .digest("hex"); + + return expectedHash === hash; + }; + const validateRequest: CsrfRequestValidator = (req) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const csrfTokenHash = getTokenHashFromRequest(req); - // This is the csrfTokenHash previously set on the response cookie via generateToken - if (typeof csrfTokenHash !== "string") return false; + const csrfCookie = getCsrfCookieFromRequest(req); + if (typeof csrfCookie != "string") return false; + + // cookie has the form {token}|{hash} + const [csrfToken, csrfTokenHash] = csrfCookie.split("|"); // csrf token from the request const csrfTokenFromRequest = getTokenFromRequest(req) as string; - // Hash the token with the provided secret and it should match the previous hash from the cookie - const expectedCsrfTokenHash = createHash("sha256") - .update(`${csrfTokenFromRequest}${getSecret(req)}`) - .digest("hex"); + const csrfSecret = getSecret(req); - return csrfTokenHash === expectedCsrfTokenHash; + return ( + csrfToken === csrfTokenFromRequest && + validateTokenAndHashPair(csrfTokenFromRequest, csrfTokenHash, csrfSecret) + ); }; const doubleCsrfProtection: doubleCsrfProtection = (req, res, next) => { diff --git a/src/tests/testsuite.ts b/src/tests/testsuite.ts index 410b863..d7ae297 100644 --- a/src/tests/testsuite.ts +++ b/src/tests/testsuite.ts @@ -51,7 +51,6 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => { it("should attach a hashed token to the request and return a token", () => { const { mockRequest, hashedToken, setCookie } = generateMocksWithTokenIntenral(); - const cookieHash = signed ? `s:${sign(hashedToken as string, mockRequest.secret as string)}` : hashedToken; diff --git a/src/tests/utils/mock.ts b/src/tests/utils/mock.ts index 30d7cab..4499603 100644 --- a/src/tests/utils/mock.ts +++ b/src/tests/utils/mock.ts @@ -89,7 +89,8 @@ export const generateMocksWithToken = ({ parse(mockRequest.headers.cookie)[cookieName], mockRequest.secret as string ) - : cookieValue; + : // signedCookie already decodes the value, but we need it if it's not signed. + decodeURIComponent(cookieValue); // Have to delete the cookies object otherwise cookieParser will skip it's parsing. delete mockRequest["cookies"]; cookieParserMiddleware(mockRequest, mockResponse, next); From 680c1611f3fa6894efb9358c17883ad0d73f9a17 Mon Sep 17 00:00:00 2001 From: David
- To create a route which generates a CSRF token and hash cookie:
+ To create a route which generates a CSRF token and a cookie containing ´${token|tokenHash}´
:
If you plan on using express-session
then please ensure your cookie-parser
middleware is registered after express-session
, as express session parses it's own cookies and may cionflict.
The generateToken
function serves the purpose of establishing a CSRF (Cross-Site Request Forgery) protection mechanism by generating a token and associated cookie. This function also provides the option to utilize a third parameter called overwrite
. By default, this parameter is set to true.
It returns a CSRF token and attaches a cookie to the response object. The cookie content is `${token}|${tokenHash}`
.
You should only transmit your token to the frontend as part of a response payload, do not include the token in response headers or in a cookie, and do not transmit the token hash by any other means.
+When overwrite
is set to false, the function behaves in a way that preserves the existing CSRF cookie and its corresponding token and hash. In other words, if a valid CSRF cookie is already present in the incoming request, the function will reuse this cookie along with its associated token.
On the other hand, if overwrite
is set to true, the function will generate a new token and cookie each time it is invoked. This behavior can potentially lead to certain complications, particularly when multiple tabs are being used to interact with your web application. In such scenarios, the creation of new cookies with every call to the function can disrupt the proper functioning of your web app across different tabs, as the changes might not be synchronized effectively (you would need to write synchronization logic in your frontend).
fastify/csrf-protection
recommendations for secret security.
secure
and signed
as true in production.
+ Do keep secure
as true in production.
- You will need to be using cookie-parser and the middleware should be registered before Double CSRF. This utility will set a cookie containing both the csrf token and a hash of the csrf token and provide the non-hashed csrf token so you can include it within your response. + You will need to be using cookie-parser and the middleware should be registered before Double CSRF. In case you want to use signed CSRF cookies, you will need to provide cookie-parser with a unique secret for cookie signing. This utility will set a cookie containing both the csrf token and a hash of the csrf token and provide the non-hashed csrf token so you can include it within your response.
-Requires TypeScript >= 3.8
+If you're using TypeScript, requires TypeScript >= 3.8
``` npm install cookie-parser csrf-csrf @@ -181,16 +181,29 @@ const doubleCsrfUtilities = doubleCsrf({The generateToken
function serves the purpose of establishing a CSRF (Cross-Site Request Forgery) protection mechanism by generating a token and associated cookie. This function also provides the option to utilize a third parameter called overwrite
. By default, this parameter is set to true.
It returns a CSRF token and attaches a cookie to the response object. The cookie content is `${token}|${tokenHash}`
.
You should only transmit your token to the frontend as part of a response payload, do not include the token in response headers or in a cookie, and do not transmit the token hash by any other means.
-When overwrite
is set to false, the function behaves in a way that preserves the existing CSRF cookie and its corresponding token and hash. In other words, if a valid CSRF cookie is already present in the incoming request, the function will reuse this cookie along with its associated token.
On the other hand, if overwrite
is set to true, the function will generate a new token and cookie each time it is invoked. This behavior can potentially lead to certain complications, particularly when multiple tabs are being used to interact with your web application. In such scenarios, the creation of new cookies with every call to the function can disrupt the proper functioning of your web app across different tabs, as the changes might not be synchronized effectively (you would need to write synchronization logic in your frontend).
By default if a csrf-csrf cookie already exists on an incoming request, generateToken will not overwrite it, it will simply return the existing token. If you wish to force a token generation, you can use the third parameter:
+ +``` +generateToken(res, req, true); // This will force a new token to be generated, and a new cookie to be set, even if one already exists +``` + +Instead of importing and using generateToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on your incoming request.
+ +``` +req.csrfToken(); // same as generateToken(res, req) and generateToken(res, req, false); +req.csrfToken(true); // same as generateToken(res, req, true); +``` + +The generateToken
function serves the purpose of establishing a CSRF (Cross-Site Request Forgery) protection mechanism by generating a token and an associated cookie. This function also provides the option to utilize a third parameter called overwrite
. By default, this parameter is set to false.
It returns a CSRF token and attaches a cookie to the response object. The cookie content is `${token}|${tokenHash}`
.
You should only transmit your token to the frontend as part of a response payload, do not include the token in response headers or in a cookie, and do not transmit the token hash by any other means.
+When overwrite
is set to false, the function behaves in a way that preserves the existing CSRF cookie and its corresponding token and hash. In other words, if a valid CSRF cookie is already present in the incoming request, the function will reuse this cookie along with its associated token.
On the other hand, if overwrite
is set to true, the function will generate a new token and cookie each time it is invoked. This behavior can potentially lead to certain complications, particularly when multiple tabs are being used to interact with your web application. In such scenarios, the creation of new cookies with every call to the function can disrupt the proper functioning of your web app across different tabs, as the changes might not be synchronized effectively (you would need to write your own synchronization logic).