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: truein CORS config amplifies CSRF risk — this application was configured to accept credentialed cross-origin requests, making the absence of CSRF middleware especially dangerous.cookie-parsermust be registered beforecsurf— the middleware order inserver.jsmatters;csurf({ cookie: true })depends on parsed cookies being available onreq.- Global middleware beats per-route protection — registering
app.use(csrfProtection)inserver.jscovers 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-usageidentified 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 })andcookie-parseras global middleware inbackend/server.js, and declared both packages inbackend/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.