npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cookie-csrf

v0.3.0

Published

A small, stateless pre-session CSRF protection middleware for Express unauthenticated routes

Downloads

308

Readme

cookie-csrf NPM Version Downloads

A lightweight, stateless pre-session CSRF protection middleware for Express, implementing OWASP's Signed Double-Submit Cookie pattern — but without binding the token to a session. Designed for unauthenticated mutating routes; typically the login form.

When to use this vs small-csrf

cookie-csrf is a companion to small-csrf, not a replacement. Load both:

| Library | Binds token to | Use on | | ------------- | --------------------------------- | ----------------------------------------------- | | small-csrf | req.session.id | Authenticated routes (the default) | | cookie-csrf | a self-minted signed cookie nonce | Unauthenticated routes only (e.g. /login) |

** Security caveat ** cookie-csrf is weaker than session-bound CSRF. Its HMAC signature only defends against cookie injection; it does not bind the token to a user identity. That is acceptable for the pre-auth case (whose real threat, login-CSRF, is stopped by the browser cookie jar + SameSite), but it means: use it only on unauthenticated routes, and rotate to small-csrf the moment the user logs in.

Introduction

cookie-csrf implements the OWASP Signed Double-Submit Cookie pattern, but mints a signed, stateless nonce cookie instead of reading req.session. This means GET /login doesn't involved the session store (avoiding a session db write).

  • No session dependency — safe to use with saveUninitialized: false
  • Constant-time token comparison to prevent timing attacks
  • Distinct request accessor, cookie key, param, and headers to avoid collisions with small-csrf
  • Zero runtime dependencies, ESM, Node ≥ 20

Installation

npm install cookie-csrf

Quick Start

The intended setup runs both middlewares — cookie-csrf on the pre-auth login flow (route-level), small-csrf on the authenticated area:

// npm install express express-session cookie-parser cookie-csrf small-csrf
import express from "express";
import session from "express-session";
import cookieParser from "cookie-parser";
import cookieCsrfProtection from "cookie-csrf";
import csrfProtection from "small-csrf";

const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(
  session({
    secret: "your-session-secret",
    resave: false,
    saveUninitialized: false, // no session row for anonymous visitors
    cookie: { secure: process.env.NODE_ENV === "production" },
  }),
);

// Pre-auth CSRF (route-level, never global, never touches the session)
const cookieCsrf = cookieCsrfProtection({
  secret: "at-least-32-characters-long-pre-secret",
});

// Authenticated-area CSRF (session-bound)
const sessionCsrf = csrfProtection({
  secret: "a-different-32-plus-char-session-secret",
});

// --- Login flow: cookie-csrf, uses preCsrfToken() / _csrf_pre ---
app.get("/login", cookieCsrf, (req, res) => {
  res.send(`
    <form action="/login" method="POST">
      <input type="hidden" name="_csrf_pre" value="${req.preCsrfToken()}">
      <input name="username"><input type="password" name="password">
      <button type="submit">Login</button>
    </form>
  `);
});

app.post("/login", cookieCsrf, (req, res) => {
  // ...authenticate...
  if (!valid) {
    // Bad password: re-render the form. preCsrfToken() rotates the cookie and
    // returns a matching token, so the re-rendered form's next submit succeeds.
    return res.status(401).send(`
      <p>Invalid username or password.</p>
      <form action="/login" method="POST">
        <input type="hidden" name="_csrf_pre" value="${req.preCsrfToken()}">
        <input name="username"><input type="password" name="password">
        <button type="submit">Login</button>
      </form>
    `);
  }
  res.clearCookie("csrf_pre_token"); // retire the pre-auth token
  req.session.user = { name: req.body.username }; // now a real session exists
  req.session.save(() => res.redirect("/dashboard"));
});

// --- Authenticated area: small-csrf, uses csrfToken() / _csrf ---
app.get("/dashboard", sessionCsrf, (req, res) => {
  res.send(`csrf token: ${req.csrfToken()}`);
});

// One handler catches CSRF errors from BOTH (same error code)
app.use((err, req, res, next) => {
  if (err.code === "EBADCSRFTOKEN") {
    return res.status(403).send("Invalid CSRF token. Please try again.");
  }
  next(err);
});

app.listen(3000);

Why not just use small-csrf on /login?

small-csrf binds each token to req.session.id. For that ID to be stable between GET /login (render) and POST /login (submit) under saveUninitialized: false, the app must dirty the session on the GET — the req.session.initialized = true workaround.

That write defeats the point of saveUninitialized: false: every unauthenticated visitor gets a persisted session row, so an attacker (or a crawler) can hammer GET /login and exhaust the session store / rate limits before anyone authenticates.

cookie-csrf allows the session dependency for the pre-auth route to be removed, so GET /login creates no session and sends no connect.sid.

Security model (and its limits)

  • The barrier that actually stops CSRF is the browser cookie jar + SameSite: an attacker can't read the victim's HttpOnly cookie and can't plant one cross-site, so they can't put a matching token in a forged form.
  • The HMAC signature only buys defense against cookie injection (a sibling subdomain / MITM writing a cookie value the attacker knows). It does not bind the token to a user identity.
  • This is fine for the pre-auth case, whose real threat is login-CSRF (covered by the cookie-jar + SameSite barrier). It is weaker than small-csrf — so use it only on unauthenticated routes and rotate on login.

Token reuse on safe requests (and the multi-tab / prefetch caveat it fixes)

A safe request (GET/HEAD/OPTIONS) reuses the existing csrf_pre_token cookie if the browser already presents one and it's well-formed (HMAC matches), rather than minting a fresh nonce on every single request. A fresh nonce is only minted when there's no cookie yet, or the presented one is malformed.

This matters because modern Chromium-based browsers (Chrome, Edge, Brave,Vivaldi, Opera) routinely fire extra, invisible, credentialed GETs to a URL the user is merely likely to visit next (prefetch/prerender), independent of "View Source", retried connections, or a second tab open on the same form.

Without reuse, every one of those incidental GETs would silently rotate the cookie out from under the page the user is actually looking at, orphaning its embedded token and 403ing the next real submit — this used to be documented here as the "multi-tab caveat"; it's now fixed for all of these cases, not just the two-tabs one.

Reuse has no effect on the security model: isWellFormed only checks that the cookie's HMAC matches its random value (a shape check), never that it's the right token for the current visitor. The check that actually matters —recomputing and comparing the HMAC, then the double-submit comparison — still runs in full on every state-changing request via verifyToken(); a forged cookie that happens to be well-formed is still rejected there.

Sliding maxAge: because reuse re-issues the cookie (same value, fresh Set-Cookie), cookie.maxAge measures idle time since the last safe request, not time since the token was first minted. A tab left open with periodic incidental GETs (prefetch, polling, etc.) keeps its token alive indefinitely rather than expiring on a hard 1-hour cap from mint. The token still only ever dies by: expiring after maxAge of genuine inactivity, or being consumed and rotated by a verified POST/PUT/PATCH/DELETE.

Process

  1. On a safe request (GET/HEAD/OPTIONS): if the browser presents an existing, well-formed csrf_pre_token cookie, it's reused (see [Token reuse on safe requests (#token-reuse-on-safe-requests-and-the-multi-tab--prefetch-caveat-it-fixes)); otherwise a cryptographically strong random nonce is generated and HMAC-signed. Either way the token is:
    • set as an HTTP-only cookie (csrf_pre_token by default), and
    • exposed via req.preCsrfToken() for inclusion in forms or AJAX.
  2. On a state-changing request (POST/PUT/PATCH/DELETE) the middleware:
    • recomputes the HMAC from the cookie's random value and checks it (rejects cookie injection),
    • checks the submitted token equals the cookie token (double-submit),
    • both comparisons are constant-time.
  3. After verification passes, req.preCsrfToken() rotates the token: the first time a handler calls it, a fresh nonce is minted, the cookie is refreshed, and the matching token is returned — so re-rendering a form (e.g. the login form after a bad password) "just works". Rotation is lazy: a handler that verifies and then redirects (a successful login) never calls the accessor, so no cookie is set.

API Reference

cookieCsrfProtection(options)

Creates and returns the CSRF middleware function.

Options

| Option | Type | Default | Description | | ----------------- | -------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secret | String | required | Secret key used for HMAC signature (must be at least 32 characters) | | cookie.key | String | "csrf_pre_token" | Name of the cookie storing the CSRF token | | cookie.path | String | "/" | Path for the CSRF cookie | | cookie.httpOnly | Boolean | true | Whether the cookie is HTTP only | | cookie.sameSite | String | "strict" | SameSite policy for the cookie ("strict", "lax", or "none") | | cookie.secure | Boolean | true | Whether the cookie requires HTTPS | | cookie.maxAge | Number | 3600000 | Max age of the cookie in milliseconds (1 hour default). Reissued (sliding) on every safe request that reuses the token — see Token reuse | | ignoreMethods | Array | ["GET", "HEAD", "OPTIONS"] | HTTP methods that don't need CSRF validation | | csrfParam | String | "_csrf_pre" | Name of the parameter containing the CSRF token in requests | | value | Function | reads body[csrfParam]x-pre-csrf-tokenx-xsrf-pre-token | Custom extractor for the submitted token (never the query string) |

req.preCsrfToken()

Function added to the request object that returns the current pre-auth CSRF token. Use this to include the token in your login form or AJAX requests.

The contract is uniform across methods: req.preCsrfToken() always returns a token that matches the csrf_pre_token cookie set on the response. On safe methods (GET/HEAD/OPTIONS) the cookie is set unconditionally. On verified state-changing methods (POST/PUT/PATCH/DELETE) calling the accessor rotates the token — minting a fresh nonce and refreshing the cookie — which is exactly what you want when re-rendering a form after a validation failure. The token is effectively single-use: each accepted submit that re-renders rotates the nonce.

Note the deliberate renames vs small-csrf so the two never collide when loaded together:

| Surface | small-csrf | cookie-csrf | | ---------------- | ------------------------------ | ------------------------------------------------- | | Request accessor | req.csrfToken() | req.preCsrfToken() | | Cookie key | csrf_token | csrf_pre_token | | Form param | _csrf | _csrf_pre | | Headers accepted | x-csrf-token, x-xsrf-token | x-pre-csrf-token, x-xsrf-pre-token | | Error code | EBADCSRFTOKEN | EBADCSRFTOKEN (same — one handler catches both) |

Security Considerations

For maximum security:

  • Use HTTPS in production environments
  • Use a cryptographically strong secret (at least 32 characters)
  • Rotate on login: res.clearCookie("csrf_pre_token") and switch the authenticated area to small-csrf
  • Set appropriate sameSite and secure cookie options (strict + secure: true)

Tests

Uses the built-in Node test runner - available from Node 20 npm test to run

Example App

To run a local demo of the cookie-csrf + small-csrf combo from a cloned repo:

cd example
npm install
npm start

Then visit http://localhost:3000. Observe that GET /login sets csrf_pre_token but no connect.sid; after login the pre-auth cookie is cleared and the dashboard is protected by small-csrf's csrf_token.

License

MIT

Credits

This is basically a copy of small-csrf with the session binding removed. A valid alternative approach might have been to add a session-less mode to small-csrf, but since it's so small (a single 130 line file) I judged that it was better to have it very clear in my projects which csrf was being applied where, and eliminate the chance of small-csrf being used in a weaker mode without the developer noticing.

small-csrf is basically a JS implementation of the OWASP CSRF Cheat sheet, so apart from not binding to the session, that's also the intention of this library.

AI Disclosure

AI tools were used in this project.

Versions

  • 0.1.0 - initial
  • 0.2.0 - req.preCsrfToken() now rotates the token on verified state-changing requests (lazy), giving a uniform "always matches the cookie" contract so re-rendering a form after a validation failure works without reflecting the submitted token
  • 0.3.0 - Safe requests (GET/HEAD/OPTIONS) reuse an existing well-formed csrf_pre_token cookie instead of unconditionally minting a fresh one.