Skip to content

csurf is archived — CSRF protection in Express

Severity: highBehaviour changed in csurf 1.11.0 (archived 2020)Applies to: Express 4.xApplies to: Express 5.xFacts last verified 2026-07-26 against csurf 1.11.0 (npm) · Express 5.2.1

Express has never shipped CSRF protection. For a decade the answer was csurf, and that answer is dead: csurf’s last release is 1.11.0, published 2020-01-19, and it carries an npm deprecation notice saying the package is archived and no longer maintained.

It still installs. It still works. npm install csurf prints one line of warning and then behaves exactly as the tutorials say, which is why it is still in production in a great many applications and still recommended by a great many guides.

if you see this
This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions

npm's deprecation notice for csurf. It prints on install and is the only warning most projects ever get.

Read in: npm registry — csurf@1.11.0 deprecated field

Decide deliberately, because there is no default

Section titled “Decide deliberately, because there is no default”

Unlike the rest of this cluster there is no single correct fix, so here is the honest landscape rather than a pick.

SameSite cookies do most of the work now. sameSite: "lax" — which express-session sets if you tell it to, and which modern browsers increasingly default to — means the session cookie is not sent on cross-site POSTs at all. For a form-based application on a single origin, that closes the classic attack without any token library. It is not complete: it does nothing for GET requests that change state (which should not exist anyway), and it depends on the browser.

Token libraries, all currently maintained, if you need defence in depth or support older clients:

Package Latest Pattern
csrf-csrf 4.0.3 (2025-05-27) double-submit cookie
csrf-sync 4.2.1 (2025-05-10) synchroniser token, session-backed
@dr.pogodin/csurf 1.17.1 (2026-06-07) maintained fork of csurf’s API

The fork is the least-disruptive migration if you have csurf wired in already, since it keeps the API. csrf-csrf is the usual choice for a stateless API.

the fix — pick one, then remove csurf
Terminal window
npm uninstall csurf
npm install csrf-csrf # or csrf-sync, or @dr.pogodin/csurf
// Whatever you choose, the SameSite baseline is worth having regardless.
app.use(
session({
// ...
cookie: { secure: true, httpOnly: true, sameSite: "lax" },
})
);
verify it workedRun this in: shell
Terminal window
npm ls csurf

An empty result is what you want. If csurf is present, note that it may be a transitive dependency of something else rather than a direct one — read the tree before assuming it is yours to remove.

verify it worked (behaviour)Run this in: http response
Terminal window
curl -si -X POST https://example.com/account/email \
-H 'Cookie: connect.sid=<a-valid-session>' \
-d 'email=attacker@example.test' | head -1

A state-changing POST carrying a valid session but no CSRF token, and no Origin/Referer from your own site, should be rejected. If it succeeds, the protection is not working regardless of what is installed.

before you ship this

Adding token-based CSRF protection breaks every client that submits without a token — your own AJAX code first, then any mobile app or integration posting to the same endpoints. That is the main reason projects postpone this and then never do it.

Stage it: run the middleware in a report-only or logging mode if the library supports one, or apply it to a single route and expand. Deploying it across every POST at once is how you take checkout down.

If you rely on sameSite: "strict" instead of tokens, remember it also breaks inbound links from other sites — see session cookies.

  • Session cookie flags — where sameSite is set
  • Django takes the opposite approach and ships CSRF protection on by default — CSRF_TRUSTED_ORIGINS is what you configure there instead of what you install.