CSURF

Latest NPM Release NPM Downloads CircleCI GitHub Repo stars Dr. Pogodin Studio

Node.js CSRF protection middleware for ExpressJS.

Sponsor

Contributors


+ contributors of the original csurf


This is a fork of the original csurf package which was deprecated by its author with doubtful reasoning (in the nutshell the package was alright, but author did not want to maintain it anymore). It is published to NPM as @dr.pogodin/csurf, its version 1.11.0 exactly matches the same, latest version of the original package, its versions starting from 1.12.0 have all dependencies updated to their latest versions, and misc maintenance performed as needed. To migrate from the original csurf just replace all references to it by @dr.pogodin/csurf.


Content

CSRF Demystified

Crux of the problem — if browser knows an authentication cookie for your domain, it may send it along with all HTTP(S) requests to your backend, even with those triggered automatically by completely unrelated websites; thus allowing a malicious third party to make authenticated requests to your API on behalf of the user, if he has loaded their page into his browser (i.e. a bad actor does not have to know those cookies, it may just rely on the user's browser sending them automatically, directly to your API).

It happens if you have opted for this (SameSite=None — bad idea), or your user uses an outdated, weird browser, as up until recently SameSite=None was the default cookie setting (e.g. Chrome changed it to SameSite=Lax only in 2020). Naturally, people who selected such default behavior (SameSite=None) have since moved to senior positions, and now make six numbers teaching you about security, while you fall for the pitfalls they created (i.e. they selected that default with tracking applications of third-party cookies in mind, and they had not thought what does it mean for possible authentication applications of cookies).

Three main ways to protect against CSRF, implemented by this library, in the nutshell all verify that the initiator of HTTP(S) requests is a part (legit or not) of your website running in the user's browser, i.e. it is actually able to read (or write) your cookies (as browser only allows the frontend to access cookies from the same origin). Mind, speaking of HTTP(S) requests we actually assume that you only use HTTPS — if you are careless enough to permit unsecure HTTP connection to your backend, why would you care about CSRF at all?

  • Synchronizer Token Pattern approach consists of the server generating a random token for the current user session, storing it at the server side, and also passing it to the frontend along with the HTML page. To make an API request the frontend is expected to read that token, and to pass it back to the server as a part of the request payload, or in its header. The server compares the token value received with the request to the value stored at the backend for that user session, and if they match it proves that the request initiator is a part of the page, able to read cookies set for that origin, or the actual HTML served for that user. Sure, a malicious code injected inside your page (XSS) will be able to do just the same, and thus to bypass your CSRF protection, however if you allow such injectsion, with or without CSRF protection, you are fucked.

    This mode of CSRF protection is provided by this library when no cookie option is set. It is also implemented somewhat smarter, as instead of storing on the server side the actual tokens issued to the user, it instead generates, and stores a random cryptographic secret for that user session, and then uses it to generate random, signed tokens, and to later verify these tokens when they are passed back to the server with API requests.

  • Naive Double-Submit Cookie Pattern approach consists of the server generating a random token for the current user session, and just passing it to the frontend via cookie. To make an API request the frontend is expected to read that token, and to pass it back to the server as a part of the request payload, or in its header, while the browser also includes into the request the original cookie. The server compares these two token values (the one received in the cookie, with the one included into the request payload or header), and if they match it proves that the request intiator is a part of the page, able to read (or write) cookies set for that origin. Beside the danger of XSS defeating this protection; if a bad actor controls (or XSS'es) a sub-domain of protected domain, he will be able to defeat it, because he can overwrite the original cookie set by the server for protected domain, and thus bypass CSRF protection just by sending along with request his own pair of matching cookie (token) values.

    This mode of CSRF protection is provided by this library when cookie option is set true. It is also implemented somewhat differently, to reuse the same code and logic used for the previous mode — it generates a random secret for user session (though, it is not quite a secret, it does not have to be in this scenario), then it generates a matching random token, signed by that secret, and it sends them both to the frontend via two different cookies. To make requests, the frontend is expected to read the token from one of these cookies, and to include it into the request payload, or header. Upon receiving the request server verifies that the token received in the request payload (header) is signed by the secret received via the corresponding cookie. Once again, this check does not rely at all on that «secret» being secret, it just cares whether that «secret» and the token are a matching pair or not — it would provide the same protection if the frontend would just pass inside the request the «secret» value itself, to be directly compared to its value in the cookie.

  • Signed Double-Submit Cookie approach is essentially the same, but it requires the cookie with token to be signed and verified on the server side. This way, even if a bad actor controls a sub-domain, and thus is able to overwrite the token cookie for the protected domain, it will be detected by the server.

    This mode of CSRF protection is provided by this library when cookie option is set to the object with cookie configuration enabling the signature (signed flag). Because of the way it is implemented (the token in request is verified against the «secret» stored in one of these cookies), it is actually necessary to sign and protect from modification that «secret» cookie, to which those cookie option is actually applied. Also, as the frontend does not really need to know that «secret» to send requests, it won't hurt to also opt for HttpOnly, secure, and other security options for that cookie, although it is not quite relevant for this mode of protection to work.

The drama around the claim of csurf library being vulnerable started with this article, «A CSRF Vulnerability in The Popular CSURF Package». The problem is that security «expert» who wrote it does not quite understand neither CSRF protection, nor JavaScript, thus he just got confused with the library implementation details, briefly explained above ¯\_(ツ)_/¯ Essentially, he argues that with the simple cookie: true setting the library does not correctly implement Signed Double-Submit Cookie protection, as it does not sign the cookies it sets, it does not protect the «secret» cookie, and it does not check the value of cookie with token in subsequent API requests. Indeed, with that setting it correctly implements a variant of Naive Double-Submit Cookie protection, with its limitations not affecting most of simple websites.

That article ends with a misleading claim that library developers acknowledged the bug, and deprecated the library because of it; while in reality the deprecation note in GitHub repo read «This npm module is currently deprecated due to the large influx of security vulunerability reports received, most of which are simply exploiting the underlying limitations of CSRF itself. The Express.js project does not have the resources to put into this module, which is largely unnecessary for modern SPA-based applications.» — essentially saying «the library is fine, but we do not want to spend our time replying to each idiot who complains about this library's vulnerabilities, without understanding the matter».

In reality, the most dangerous moment about it was that deprecation of the original library, and re-telling of that article around Internet, encouraged developers to switch from this, long-standing CSURF library to one of many new CSRF-protection libraries written from scratch by different people. This both opens opportunities for dependency poisioning (i.e. somebody creates a new CSRF-protection library with some malicious code embedded in), or for honest mistakes (be sure, this long standing library had more eyes revising its implementation than any of its new alternatives out there). That's why this fork of the original CSURF came into existence — instead of trying my luck with a new alternative, I just forked this time-proven library, reviewed its code, just in case, updated dependencies, and converted the source codebase to TypeScript (just for the ease of future maintenance). The project is open-source, thus everybody is welcome to additionally review it and suggest any improvements or fixes.

Installation

Requires either a session middleware or cookie-parser to be initialized first.

If you have questions on how this module is implemented, please read Understanding CSRF.

This is a Node.js module available through the npm registry. Installation is done using the npm install command:

$ npm install --save @dr.pogodin/csurf

API

import csurf from '@dr.pogodin/csurf';

csurf([options])

Create a middleware for CSRF token creation and validation. This middleware adds a req.csrfToken() function to make a token which should be added to requests which mutate state, within a hidden form field, query-string etc. This token is validated against the visitor's session or csrf cookie.

Options

The csurf function takes an optional options object that may contain any of the following keys:

cookie

Determines if the token secret for the user should be stored in a cookie or in req.session. Storing the token secret in a cookie implements the double submit cookie pattern. Defaults to false.

When set to true (or an object of options for the cookie), then the module changes behavior and no longer uses req.session. This means you are no longer required to use a session middleware. Instead, you do need to use the cookie-parser middleware in your app before this middleware.

When set to an object, cookie storage of the secret is enabled and the object contains options for this functionality (when set to true, the defaults for the options are used). The options may contain any of the following keys:

  • key - the name of the cookie to use to store the token secret (defaults to '_csrf').
  • path - the path of the cookie (defaults to '/').
  • signed - indicates if the cookie should be signed (defaults to false).
  • secure - marks the cookie to be used with HTTPS only (defaults to false).
  • maxAge - the number of seconds after which the cookie will expire (defaults to session length).
  • httpOnly - flags the cookie to be accessible only by the web server (defaults to false).
  • sameSite - sets the same site policy for the cookie(defaults to false). This can be set to 'strict', 'lax', 'none', or true (which maps to 'strict').
  • domain - sets the domain the cookie is valid on(defaults to current domain).
ignoreMethods

An array of the methods for which CSRF token checking will disabled. Defaults to ['GET', 'HEAD', 'OPTIONS'].

sessionKey

Determines what property ("key") on req the session object is located. Defaults to 'session' (i.e. looks at req.session). The CSRF secret from this library is stored and read as req[sessionKey].csrfSecret.

If the "cookie" option is not false, then this option does nothing.

value

Provide a function that the middleware will invoke to read the token from the request for validation. The function is called as value(req) and is expected to return the token as a string.

The default value is a function that reads the token from the following locations, in order:

  • req.body._csrf - typically generated by the body-parser module.
  • req.query._csrf - a built-in from Express.js to read from the URL query string.
  • req.headers['csrf-token'] - the CSRF-Token HTTP request header.
  • req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
  • req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
  • req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.

Examples

Simple ExpressJS Example

The following is an example of some server-side code that generates a form that requires a CSRF token to post back.

import cookieParser from 'cookie-parser';
import csrf from '@dr.pogodin/csurf';
import bodyParser from 'body-parser';
import express from 'express';

// setup route middlewares
const csrfProtection = csrf({ cookie: true })
const parseForm = bodyParser.urlencoded({ extended: false })

// create express app
const app = express()

// parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser())

app.get('/form', csrfProtection, function (req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() })
})

app.post('/process', parseForm, csrfProtection, function (req, res) {
  res.send('data is being processed')
})

Inside the view (depending on your template language; handlebars-style is demonstrated here), set the csrfToken value as the value of a hidden input field named _csrf:

<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  
  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>

Using AJAX

When accessing protected routes via ajax both the csrf token will need to be passed in the request. Typically this is done using a request header, as adding a request header can typically be done at a central location easily without payload modification.

The CSRF token is obtained from the req.csrfToken() call on the server-side. This token needs to be exposed to the client-side, typically by including it in the initial page content. One possibility is to store it in an HTML <meta> tag, where value can then be retrieved at the time of the request by JavaScript.

The following can be included in your view (handlebar example below), where the csrfToken value came from req.csrfToken():

<meta name="csrf-token" content="{{csrfToken}}">

The following is an example of using the Fetch API to post to the /process route with the CSRF token from the <meta> tag on the page:

// Read the CSRF token from the <meta> tag
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

// Make a request using the Fetch API
fetch('/process', {
  credentials: 'same-origin', // <-- includes cookies in the request
  headers: {
    'CSRF-Token': token // <-- is the csrf token as a header
  },
  method: 'POST',
  body: {
    favoriteColor: 'blue'
  }
})

Single Page Application (SPA)

Many SPA frameworks like Angular have CSRF support built in automatically. Typically they will reflect the value from a specific cookie, like XSRF-TOKEN (which is the case for Angular).

To take advantage of this, set the value from req.csrfToken() in the cookie used by the SPA framework. This is only necessary to do on the route that renders the page (where res.render or res.sendFile is called in Express, for example).

The following is an example for Express of a typical SPA response:

app.all('*', function (req, res) {
  res.cookie('XSRF-TOKEN', req.csrfToken())
  res.render('index')
})

Ignoring Routes

Note CSRF checks should only be disabled for requests that you expect to come from outside of your website. Do not disable CSRF checks for requests that you expect to only come from your website. An existing session, even if it belongs to an authenticated user, is not enough to protect against CSRF attacks.

The following is an example of how to order your routes so that certain endpoints do not check for a valid CSRF token.

import cookieParser from 'cookie-parser';
import csrf from '@dr.pogodin/csurf';
import bodyParser from 'body-parser';
import express from 'express';

// create express app
const app = express()

// create api router
const api = createApiRouter()

// mount api before csrf is appended to the app stack
app.use('/api', api)

// now add csrf and other middlewares, after the "/api" was mounted
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(csrf({ cookie: true }))

app.get('/form', function (req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() })
})

app.post('/process', function (req, res) {
  res.send('csrf was required to get here')
})

function createApiRouter () {
  const router = new express.Router()

  router.post('/getProfile', function (req, res) {
    res.send('no csrf to get here')
  })

  return router
}

Custom Error Handling

When the CSRF token validation fails, an error is thrown that has err.code === 'EBADCSRFTOKEN'. This can be used to display custom error messages.

import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import csrf from '@dr.pogodin/csurf';
import express from 'express';

const app = express()
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(csrf({ cookie: true }))

// error handler
app.use(function (err, req, res, next) {
  if (err.code !== 'EBADCSRFTOKEN') return next(err)

  // handle CSRF token errors here
  res.status(403)
  res.send('form tampered with')
})
© Dr. Pogodin Studio, 2018–2025 — ‌doc@pogodin.studio‌ — ‌Terms of Service