diff --git a/README.md b/README.md index ee9e9c6..db8375c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,14 @@ -

- Double CSRF -

- -

A utility package to help implement stateless CSRF protection using the Double Submit Cookie Pattern in express.

- -

- - - - - - - - - -

+# @otterjs/csrf-csrf + +**Double-submit cookie pattern CSRF protection middleware for modern Node.js.** + +> :pushpin: This project is a fork of [Psifi-Solutions/csrf-csrf](https://github.com/Psifi-Solutions/csrf-csrf). -

+[![npm][npm-img]][npm-url] +[![GitHub Workflow Status][gh-actions-img]][github-actions] +[![Coverage][cov-img]][cov-url] + +

Dos and Don'tsGetting StartedConfiguration • @@ -24,76 +16,48 @@ Support

-

Background

- -

- This module provides the necessary pieces required to implement CSRF protection using the Double Submit Cookie Pattern. This is a stateless CSRF protection pattern, if you are using sessions and would prefer a stateful CSRF strategy, please see csrf-sync for the Synchroniser Token Pattern. -

- -

- Since csurf has been deprecated I struggled to find alternative solutions that were accurately implemented and configurable, so I decided to write my own! Thanks to NextAuth as I referenced their implementation. From experience CSRF protection libraries tend to complicate their configuration, and if misconfigured, can render the protection completely useless. - +## Background -

- This is why csrf-csrf aims to provide a simple and targeted implementation to simplify it's use. -

+This module provides the necessary pieces required to implement CSRF protection using the +[Double Submit Cookie Pattern][owasp-csrf-dsc]. This is a stateless CSRF protection pattern, if you are using +sessions and would prefer a stateful CSRF strategy, please see [csrf-sync](https://github.com/Psifi-Solutions/csrf-sync) +for the [Synchroniser Token Pattern][owasp-csrf-st].

Dos and Don'ts

- + +- **Do** read the [OWASP - Cross-Site Request Forgery Prevention Cheat Sheet][owasp-csrf]. +- **Do** follow the [configuration recommendations](#configuration). +- **Do** follow `fastify/csrf-protection` [recommendations for secret security][fastify-csrf-secret-security]. +- **Do** ensure your cookies are set to `secure: true` in production. +- **Do** make sure you follow best practises to avoid compromising your security. +- **Do not** use the same secret for `csrf-csrf` and `cookie-parser`. +- **Do not** transmit your CSRF token by cookies. +- **Do not** expose your CSRF tokens or has in any log output or transactions other than the CSRF exchange. +- **Do not** transmit the token hash by any means other than the cookie issued by the CSRF exchange.

Getting Started

-

- This section will guide you through using the default setup, which does sufficiently implement the Double Submit Cookie Pattern. If you'd like to customise the configuration, see the configuration section. -

-

- 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. -

-

If you're using TypeScript, requires TypeScript >= 3.8

+ +This section will guide you through using the default setup, which does sufficiently implement the +Double Submit Cookie Pattern. If you'd like to customise the configuration, see [configuration](#configuration). + +You will need to be using [tinyhttp/cookie-parser](https://github.com/tinyhttp/cookie-parser) whose middleware +should be registered before `csrf-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 (1) set a cookie containing both the csrf token and a hash of the csrf token, and +(2) provide the plain csrf token. +You are then responsible for including the CSRF token within your response however you choose. ``` -npm install cookie-parser csrf-csrf +npm install @tinyhttp/cookie-parser @otterjs/csrf-csrf ``` ```js // ESM -import { doubleCsrf } from "csrf-csrf"; +import { doubleCsrf } from "@otterjs/csrf-csrf"; // CommonJS -const { doubleCsrf } = require("csrf-csrf"); +const { doubleCsrf } = require("@otterjs/csrf-csrf"); ``` ```js @@ -105,32 +69,23 @@ const { } = doubleCsrf(doubleCsrfOptions); ``` -

- This will extract the default utilities, you can configure these and re-export them from your own module. 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. - -

- To create a route which generates a CSRF token and a cookie containing ´${token|tokenHash}´: -

+The `doubleCsrf` method will provide the default utilities, you can configure these and re-export them from your own module. +You should only transmit your token to the frontend as part of a response body, **do not** include the token in +response headers or in a cookie, and **do not** transmit the token hash by any other means. -```js -const myRoute = (req, res) => { - const csrfToken = generateToken(req, res); - // You could also pass the token into the context of a HTML response. - res.json({ csrfToken }); -}; -const myProtectedRoute = (req, res) => - res.json({ unpopularOpinion: "Game of Thrones was amazing" }); -``` - -

Instead of importing and using generateToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on your incoming request.

+To create a route which generates a CSRF token and a cookie containing `´${token|tokenHash}´`: ```js -request.csrfToken(); // same as generateToken(req, res); +const myCsrfExchangeRoute = (req, res) => { + const csrfToken = generateToken(req, res); + // You could also pass the token into the context of a HTML template response. + return res.json({ csrfToken }); +} ``` -

- You can also put the token into the context of a templated HTML response. Just make sure you register this route before registering the middleware so you don't block yourself from getting a token. -

+You can also put the token into the context of a templated HTML response. +If you use an HTTP verb other than `GET`, make sure you register this route before registering the +`doubleCsrfProtection` middleware so you don't block yourself from getting a token. ```js // Make sure your session middleware is registered before these @@ -141,59 +96,43 @@ express.use(doubleCsrfProtection); ```

- By default, any request that are not GET, HEAD, or OPTIONS methods will be protected. You can configure this with the ignoredMethods option. + By default, any request that are not GET, HEAD, or OPTIONS methods will be + protected. You can configure this with the ignoredMethods option.

-You can also protect routes on a case-to-case basis: + You can also protect routes on a case-to-case basis:

```js app.get("/secret-stuff", doubleCsrfProtection, myProtectedRoute); ``` -

- Once a route is protected, you will need to ensure the hash cookie is sent along with the request and by default you will need to include the generated token in the x-csrf-token header, otherwise you'll receive a `403 - ForbiddenError: invalid csrf token`. If your cookie is not being included in your requests be sure to check your withCredentials and CORS configuration. -

- -

Sessions

+Once a route is protected, you will need to ensure the hash cookie is sent along with the request and by default +you will need to include the generated token in the `x-csrf-token` header, otherwise you'll receive +a `403 - ForbiddenError: invalid csrf token`. If your cookie is not being included in your requests be sure to +check your `withCredentials` and CORS configuration. -

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 conflict.

+### Sessions -

Using asynchronously

- -

csrf-csrf itself will not support promises or async, however there is a way around this. If your csrf token is stored externally and needs to be retrieved asynchronously, you can register an asynchronous middleware first, which exposes the token.

- -```js -(req, res, next) => { - getCsrfTokenAsync(req) - .then((token) => { - req.asyncCsrfToken = token; - next(); - }) - .catch((error) => next(error)); -}; -``` - -

And in this example, your getTokenFromRequest would look like this:

- -```js -(req) => req.asyncCsrfToken; -``` +If you plan on using session middleware then please ensure your cookie-parsing middleware is +registered *after* your session middleware. +Your session middleware may parse its own cookies and therefore may conflict with your cookie parsing middleware.

Configuration

-When creating your doubleCsrf, you have a few options available for configuration, the only required option is getSecret, the rest have sensible defaults (shown below). +When configuring, the only required options are `getSecret` and `getSessionIdentifier`, the rest have sensible +defaults (shown below). ```js const doubleCsrfUtilities = doubleCsrf({ getSecret: () => "Secret", // A function that optionally takes the request and returns a secret getSessionIdentifier: (req) => req.session.id, // A function that returns the session identifier for the request - cookieName: "__Host-psifi.x-csrf-token", // The name of the cookie to be used, recommend using Host prefix. cookieOptions: { - sameSite = "lax", // Recommend you make this strict if posible - path = "/", - secure = true, + name: "__Host-otter.x-csrf-token", // The name of the cookie to be used, recommend using __Host prefix + sameSite: "lax", // Recommend you make this strict if posible + path: "/", + secure: true, ...remainingCookieOptions // See cookieOptions below }, size: 64, // The size of the generated tokens in bits @@ -202,114 +141,133 @@ const doubleCsrfUtilities = doubleCsrf({ }); ``` -

getSecret

+### `getSecret` ```ts -(request?: Request) => string | string[] +type GetSecretType = (request?: Request) => string | string[] ```

Required

-

This should return a secret key or an array of secret keys to be used for hashing the CSRF tokens.

-

In case multiple are provided, the first one will be used for hashing. For validation, all secrets will be tried, preferring the first one in the array. Having multiple valid secrets can be useful when you need to rotate secrets, but you don't want to invalidate the previous secret (which might still be used by some users) right away.

-

+This should return a secret key or an array of secret keys to be used for hashing the CSRF tokens. -

getSessionIdentifier

+In case multiple are provided, the first one will be used for hashing. +For validation, all secrets will be tried, preferring the first one in the array. +Having multiple valid secrets can be useful when you need to rotate secrets, but you don't want to invalidate +the previous secret (which might still be used by some users) right away. -```ts -(request: Request) => string; -``` - -

- Optional
- Default: -

- -``` -(req) => req.session.id -``` - -

This function should return the session identifier for the incoming request. This is used as part of the csrf token hash to ensure generated tokens can only be used by the sessions that originally requested them.

- -

If you are rotating your sessions, you will need to ensure a new CSRF token is generated at the same time. This should typically be done when a session has some sort of authorization elevation (e.g. signed in, signed out, sudo).

- -

cookieName

+### `getSessionIdentifier` ```ts -string; +type GetSessionIdentifierType = (request: Request) => string; ``` -

- Optional
- Default: "__Host-psifi.x-csrf-token"
-

- -

Optional: The name of the cookie that will be used to track CSRF protection. If you change this it is recommend that you continue to use the __Host- or __Secure- security prefix.

+

Required

-

Change for development

+This function should return the session identifier for the incoming request. +This is used as part of the CSRF token hash to ensure generated tokens can only be used by the sessions that +originally requested them. -

The security prefix requires the secure flag to be true and requires requests to be received via HTTPS, unless you have your local instance running via HTTPS, you will need to change this value in your development environment.

+If you are rotating your sessions, you will need to ensure a new CSRF token is generated at the same time. +This should typically be done when a session has some sort of authorization elevation (e.g. signed in, signed out, sudo). -

cookieOptions

+### `cookieOptions` ```ts -{ +type CookieOptions = SerializeOptions & { + name?: string sameSite?: string; path?: string; - secure?: boolean - ...remainingCookieOptions // See below. + secure?: boolean; + signed?: boolean; } ```

- Optional
+ Optional
Default:

```ts -{ +const defaultCookieOptions = { + name: "__Host-otter.x-csrf-token", sameSite: "lax", path: "/", - secure: true + secure: true, + signed: false, } ``` -

The options provided to the cookie, see cookie attributes. The remaining options are all undefined by default and consist of:

+The options used when serializing the CSRF exchange cookie +(see [cookie attributes]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie")). + +
+name + +The name of the cookie that will be used to track CSRF protection. +If you change this it is recommended that you continue to use the `__Host` or `__Secure` +[security prefix]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie"). + +
+
+ +
+signed + +Whether to sign CSRF exchange cookies. + +When this option is enabled, you also need to provide your cookie parsing middleware with a unique secret for +cookie signing. + +
+
+ +
+Development environments without HTTPS + +The `__Host` security prefix requires the `secure` flag to be `true` and requires requests to be served via HTTPS. +Unless you have your local instance running via HTTPS, you will need to change the cookie `name` in your +development environment to omit the security prefix. + +You will need to set `secure` to false unless you're running HTTPS locally. +Ensure `secure` is true in your live environment by using environment variables. + +
+
+ +The remaining options are all undefined by default and consist of (at least): ```ts +type RemainingCookieOptions = { maxAge?: number | undefined; - signed?: boolean | undefined; expires?: Date | undefined; domain?: string | undefined; encode?: (val: string) => string +} ``` -

Change for development

- -

For development you will need to set secure to false unless you're running HTTPS locally. Ensure secure is true in your live environment by using environment variables.

- -

delimiter

+### `delimiter` ```ts -string; +type DelimiterType = string; ```

- Optional
- Default: "|"
+ Optional
+ Default: "|"

The delimiter is used when concatenating the plain CSRF token with the hash, constructing the value for the cookie. It is also used when splitting the cookie value. This is how a token can be reused when there is no state. Note that the plain token value within the cookie is only intended to be used for token re-use, it is not used as the source for token validation.

-

getTokenFromRequest

+### `getTokenFromRequest` ```ts (req: Request) => string | null | undefined; ```

- Optional
- Default:
+ Optional
+ Default:

```ts @@ -318,10 +276,10 @@ string;

This function should return the token sent by the frontend, the doubleCsrfProtection middleware will validate the value returned by this function against the value in the cookie.

-

hmacAlgorithm

+### `hmacAlgorithm` ```ts -string; +type HmacAlgorithmType = string; ```

@@ -331,38 +289,44 @@ string;

The algorithm passed to the createHmac call when generating a token.

-

ignoredMethods

+### `ignoredMethods` ```ts -Array; +type IgnoredMethodsType = Array; ```

-Optional
-Default:
["GET", "HEAD", "OPTIONS"] +Optional
+Default: ["GET", "HEAD", "OPTIONS"]

-

An array of request types that the doubleCsrfProtection middleware will ignore, requests made matching these request methods will not be protected. It is recommended you leave this as the default.

+An array of request types that the `doubleCsrfProtection` middleware will ignore. +Requests made matching these request methods will not be protected. +It is recommended you leave this as the default. + -

size

+### `size` ```ts -number; +type SizeType = number; ```

- Optional
- Default:
64 + Optional
+ Default: 64

-

The size in bytes of the tokens that will be generated, if you plan on re-generating tokens consider dropping this to 32.

+The size in bytes of the tokens that will be generated. +If you plan on re-generating tokens, consider reducing this to 32. -

errorConfig

+

errorConfig

```ts -statusCode?: number; -message?: string; -code?: string | undefined; +type ErrorConfigType = { + statusCode?: number; + message?: string; + code?: string | undefined; +} ```

@@ -371,49 +335,65 @@ code?: string | undefined;

```ts -{ +const defaultErrorConfig = { statusCode: 403, message: "invalid csrf token", - code: "EBADCSRFTOKEN" + code: "ERR_BAD_CSRF_TOKEN" } ``` -Used to customise the error response statusCode, the contained error message, and it's code, the error is constructed via createHttpError. The default values match that of csurf for convenience. +Used to customise the error response `statusCode`, the contained error `message`, and its `code`. +The error is constructed with `createHttpError`.

Utilities

-

Below is the documentation for what doubleCsrf returns.

+Below is the documentation for what doubleCsrf returns. -

doubleCsrfProtection

+### `doubleCsrfProtection` ```ts -(request: Request, response: Response, next: NextFunction) => void +type DoubleCsrfProtection = (request: Request, response: Response, next: () => void) => void ``` -

The middleware used to actually protect your routes, see the getting started examples above, or the examples included in the repository.

+The middleware used to actually protect your routes (see the 'getting started' examples above +, or the examples included in the repository). -

generateToken

+### `generateToken` ```ts -( +type GenerateTokenType = ( request: Request, response: Response, - { + options?: { cookieOptions?: CookieOptions, // overrides cookieOptions previously configured just for this call overwrite?: boolean, // Set to true to force a new token to be generated validateOnReuse?: boolean, // Set to false to generate a new token if token re-use is invalid - } // optional + } ) => string; ``` -

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 so long as the token is valid. If you wish to force a token generation, you can use the third parameter:

+The function that establishes a CSRF (Cross-Site Request Forgery) protection mechanism by generating a token and issuing a cookie. + +It returns a CSRF token and attaches a cookie to the response object. +The cookie content is `${token}${delimiter}${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 means other than the CSRF exchange cookie. + +By default, if a `csrf-csrf` cookie already exists on an incoming request, `generateToken` will not overwrite it. +Instead, it will return the existing token so long as the token is valid. +If you wish to force a token generation, you can set the `overwrite` option: ```ts generateToken(req, res, { overwrite: true }); // This will force a new token to be generated, and a new cookie to be set, even if one already exists ``` -

If the 'overwrite' parameter is set to false (default), the existing token will be re-used and returned. However, the cookie value will also be validated. If the validation fails an error will be thrown. If you don't want an error to be thrown, you can set the 'validateOnReuse' (by default, true) to false. In this case instead of throwing an error, a new token will be generated and returned. -

+If the 'overwrite' parameter is set to false (default), the existing token will be re-used and returned. +However, the cookie value will still be validated. +If the validation fails, an error will be thrown. +If you don't want an error to be thrown, you can set the `validateOnReuse` option to `false` (it is `true` by default). +In this case, a new token will be generated and returned to replace the invalid token. ```ts generateToken(req, res, { overwrite: true }); // As overwrite is true, an error will never be thrown. @@ -421,44 +401,32 @@ generateToken(req, res, { overwrite: false }); // As validateOnReuse is true (de generateToken(req, res, { overwrite: false, validateOnReuse: false }); // As validateOnReuse is false, if the cookie is invalid a new token will be generated without any error being thrown and despite overwrite being false ``` -

Instead of importing and using generateToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on your incoming request.

+### `invalidCsrfTokenError` ```ts -req.csrfToken(); // same as generateToken(req, res); -req.csrfToken({ overwrite: true }); // same as generateToken(req, res, { overwrite: true, validateOnReuse }); -req.csrfToken({ overwrite: false, validateOnReuse: false }); // same as generateToken(req, res, { overwrite: false, validateOnReuse: false }); -req.csrfToken(req, res, { overwrite: false }); -req.csrfToken(req, res, { overwrite: false, validateOnReuse: false }); +type InvalidCsrfTokenError = Error & { + code: string +} ``` -

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, and a fourth parameter called validateOnReuse. By default, overwrite is set to false, and validateOnReuse 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 your own synchronization logic).

-

If overwrite is set to false, the function will also validate the existing cookie information. If the information is found to be invalid (for instance, if the secret has been changed from the time the cookie was generated), an error will be thrown. If you don't want an error to be thrown, you can set the validateOnReuse (by default, true) to false. If it is false, instead of throwing an error, a new cookie will be generated.

- -

invalidCsrfTokenError

- -

This is the error instance that gets passed as an error to the next call, by default this will be handled by the default error handler. This error is customizable via the errorConfig.

+This is the error instance that will be thrown should CSRF token verification fail. +This error is customizable via [`errorConfig`](#configuration-error-config). -

validateToken

+### `validateToken` ```ts -(req: Request) => boolean; +type ValidateToken = (req: Request) => boolean; ``` -

This function is used by the doubleCsrfProtection middleware to determine whether an incoming request has a valid CSRF token. You can use this to make your own custom middleware (not recommended).

- -

Support

- - +This function is used by the doubleCsrfProtection middleware to determine whether an incoming request has a valid +CSRF token. You can use this to make your own custom middleware (not recommended). -Buy Me A Coffee +[npm-url]: https://npmjs.com/package/@otterjs/csrf-csrf +[npm-img]: https://img.shields.io/npm/dt/@otterjs/csrf-csrf?style=for-the-badge&color=blueviolet +[github-actions]: https://github.com/otterjs/csrf-csrf/actions +[gh-actions-img]: https://img.shields.io/github/actions/workflow/status/otterjs/csrf-csrf/ci.yml?style=for-the-badge&logo=github&label=&color=blueviolet +[cov-url]: https://coveralls.io/github/OtterJS/csrf-csrf +[cov-img]: https://img.shields.io/coveralls/github/OtterJS/csrf-csrf?style=for-the-badge&color=blueviolet +[owasp-csrf-dsc]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie +[owasp-csrf-st]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern +[fastify-csrf-secret-security]: https://github.com/fastify/csrf-protection#securing-the-secret