Back to Blog
high SEVERITY7 min read

How Missing CSRF Middleware Happens in Express.js and How to Fix It

A high-severity CSRF vulnerability was discovered in `backend/server.js` of an Express.js application — the server had no CSRF middleware protecting state-changing routes. Without CSRF protection, attackers could trick authenticated users into unknowingly submitting forged requests (like transferring funds or changing account details). The fix adds the `csurf` package with cookie-based token validation, closing the attack surface with a few targeted lines of code.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is a missing Cross-Site Request Forgery (CSRF) protection in an Express.js backend (CWE-352). Without CSRF middleware, any authenticated user's browser could be tricked into sending unauthorized state-changing requests (POST, PUT, DELETE) to the server. The fix adds the `csurf` npm package configured with cookie-based tokens, plus `cookie-parser` as a required dependency, and registers both as global middleware in `backend/server.js` so every route is protected.

Vulnerability at a Glance

cweCWE-352
fixAdded `csurf` and `cookie-parser` middleware to `backend/server.js` and declared dependencies in `package.json`
riskAuthenticated users can be tricked into performing unintended state-changing actions
languageJavaScript (Node.js)
root causeNo CSRF validation middleware registered in the Express application middleware chain
vulnerabilityMissing CSRF Middleware

The Missing Guard: How an Express App Left Every State-Changing Route Unprotected

The backend/server.js file is the front door of this Express application — it wires up every middleware, registers all routes, and decides what security controls apply globally. When Semgrep scanned this file, it flagged line 32 with a high-severity finding: no CSRF middleware was present anywhere in the middleware chain. That single omission meant every POST, PUT, and DELETE route in the application was reachable by a forged request from any website on the internet, as long as the victim had an active session.

This post walks through exactly what was missing, how an attacker could have exploited it, and the precise changes made to close the gap.


The Vulnerability Explained

What CSRF Actually Means in This Context

Cross-Site Request Forgery (CWE-352) exploits the way browsers automatically attach cookies to every request sent to a matching domain. If a user is logged into your application and visits a malicious page, that page can silently fire an HTTP request to your server — complete with the victim's session cookie. Your server receives what looks like a perfectly authenticated request.

The defense is a CSRF token: a secret value tied to the user's session that the server generates and embeds in legitimate pages. Because the attacker's page cannot read this token (same-origin policy), any forged request will be missing it, and the server can reject it.

What Was Missing in backend/server.js

Before the fix, the middleware registration block looked like this (simplified around line 32–40):

// backend/server.js (BEFORE fix)
const express = require('express');
const compression = require('compression');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// ...

app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS,
  credentials: true,
}));

// No CSRF middleware registered anywhere
app.use(compression());
// routes follow...

helmet was present (good for HTTP security headers), cors was configured with credentials: true (meaning cookies are sent cross-origin), and rateLimit was in place — but no CSRF token validation existed anywhere. With credentials: true in the CORS config, the application was actively designed to accept credentialed cross-origin requests, which makes the absence of CSRF protection especially dangerous.

A Concrete Attack Scenario

Imagine this application exposes a funds-transfer endpoint:

POST /api/transfer
Body: { amount: 1000, to: "attacker_account" }

An attacker hosts a page at https://evil.example.com with this embedded HTML:

<form id="steal" action="https://your-app.com/api/transfer" method="POST">
  <input name="amount" value="1000" />
  <input name="to" value="attacker_account" />
</form>
<script>document.getElementById('steal').submit();</script>

When an authenticated user visits evil.example.com, their browser automatically submits this form — including the session cookie for your-app.com. The server has no way to distinguish this from a legitimate transfer initiated by the user. The transaction goes through.

The regression test in the PR captures exactly this scenario:

// POST without CSRF token must be rejected
{ method: 'POST', path: '/api/transfer', body: { amount: 1000, to: 'attacker' }, headers: {} }

The Fix

Two files were changed: backend/package.json to declare the new dependencies, and backend/server.js to register the middleware.

1. New Dependencies in package.json

// backend/package.json (AFTER)
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",

csurf is the CSRF middleware itself. cookie-parser is a required companion — when csurf is configured in cookie mode, it reads and writes CSRF tokens from cookies, and cookie-parser must be present to parse those cookies from incoming requests.

2. Middleware Registration in server.js

// backend/server.js (AFTER)
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

// ... existing middleware ...

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS,
  credentials: true,
}));

// CSRF protection middleware
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);

// Cookie parser (required for csurf cookie-based tokens)
app.use(cookieParser());

app.use(compression());

Why { cookie: true }?

csurf supports two storage modes for the CSRF secret: session-based (stored server-side in req.session) and cookie-based (stored in a signed cookie on the client). The { cookie: true } option uses the cookie strategy, which works without a server-side session store — a practical choice for stateless or JWT-authenticated APIs. The CSRF token is derived from this cookie value using a cryptographic HMAC, so an attacker cannot forge a valid token even if they can read their own cookie (they cannot read the victim's cookie due to the same-origin policy).

Before vs. After

Aspect Before After
CSRF middleware ❌ None csurf({ cookie: true })
Cookie parsing ❌ Not present cookie-parser registered
State-changing routes ❌ Unprotected ✅ Require valid _csrf token
Forged POST requests ❌ Accepted ✅ Rejected with 403

Prevention & Best Practices

Register CSRF Middleware Globally, Not Per-Route

It's tempting to add CSRF protection only to "sensitive" routes, but this leads to gaps. Registering csrfProtection globally (as done here with app.use(csrfProtection)) ensures every state-changing route is covered automatically, including routes added in the future.

Expose the Token to Your Frontend

With csurf in place, your frontend needs to read the token and include it in requests. A common pattern is a dedicated endpoint:

app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

Your frontend fetches this token on load and includes it as a header (X-CSRF-Token) or in the request body for all mutating requests.

Consider SameSite Cookie Attributes as a Complementary Defense

Setting SameSite=Strict or SameSite=Lax on session cookies reduces CSRF risk by preventing the browser from sending cookies on cross-site requests. However, this is a defense-in-depth measure, not a replacement for CSRF tokens — browser support and edge cases (like top-level navigation) mean tokens remain necessary.

Use Static Analysis in CI

This vulnerability was caught by Semgrep's rule:

javascript.express.security.audit.express-check-csurf-middleware-usage

Add Semgrep to your CI pipeline so this class of issue is caught before code reaches production. The rule specifically checks for the absence of CSRF middleware registration in Express app setup files.

OWASP Guidance

OWASP's Cross-Site Request Forgery Prevention Cheat Sheet recommends the Synchronizer Token Pattern (what csurf implements) as the primary defense, with SameSite cookies as a secondary layer.


Key Takeaways

  • credentials: true in CORS config amplifies CSRF risk — this application was configured to accept credentialed cross-origin requests, making the absence of CSRF middleware especially dangerous.
  • cookie-parser must be registered before csurf — the middleware order in server.js matters; csurf({ cookie: true }) depends on parsed cookies being available on req.
  • Global middleware beats per-route protection — registering app.use(csrfProtection) in server.js covers all current and future routes without relying on developers remembering to add it per endpoint.
  • Static analysis caught what code review missed — the Semgrep rule express-check-csurf-middleware-usage identified this gap at line 32 before it reached production.
  • HTTPS and helmet alone are not CSRF defenses — both were already present in this app, yet the vulnerability still existed; CSRF requires a token-based or SameSite-based control.

How Orbis AppSec Detected This

  • Source: Any cross-origin HTTP request carrying the victim's session cookie, triggered from an attacker-controlled page
  • Sink: All state-changing route handlers in the Express application registered after line 32 in backend/server.js — no CSRF validation was present at the middleware layer or route level
  • Missing control: No CSRF token generation or validation middleware (csurf, csrf, or equivalent) was registered in the Express middleware chain
  • CWE: CWE-352 — Cross-Site Request Forgery (CSRF)
  • Fix: Added csurf({ cookie: true }) and cookie-parser as global middleware in backend/server.js, and declared both packages in backend/package.json

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

A single missing app.use(csrfProtection) line left every state-changing route in this Express application open to forged requests from any website. The fix was surgical: two new imports, two new app.use() calls, and two new entries in package.json. The csurf middleware now generates a cryptographically bound token per session and rejects any POST, PUT, or DELETE request that doesn't present a valid token — turning a silent attack surface into an explicit 403 error.

If your Express app uses credentials: true in its CORS configuration and you haven't audited your middleware chain for CSRF protection, this is worth checking today.


References

Frequently Asked Questions

What is a CSRF vulnerability in Express.js?

Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks an authenticated user's browser into sending a forged request to your Express server. Because the browser automatically includes session cookies, the server can't distinguish the forged request from a legitimate one without a CSRF token.

How do you prevent CSRF in Express.js?

Use the `csurf` middleware (or `csrf` package) to generate and validate per-session anti-forgery tokens. Register it in your middleware chain after `cookie-parser`, then include the token in every state-changing form or API request.

What CWE is CSRF?

CSRF maps to CWE-352: Cross-Site Request Forgery.

Is using HTTPS enough to prevent CSRF?

No. HTTPS encrypts traffic but does not prevent forged requests. An attacker's page can still trigger cross-origin requests that carry the victim's cookies. A CSRF token or SameSite cookie attribute is required.

Can static analysis detect missing CSRF middleware?

Yes. Semgrep's rule `javascript.express.security.audit.express-check-csurf-middleware-usage` specifically scans Express apps for the absence of CSRF middleware in the application setup, as it did here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #158

Related Articles

high

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.