How Missing CSRF Middleware Happens in Express.js and How to Fix It
Summary
A high-severity CSRF vulnerability was discovered in libProxy.js of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a csrf token library, a /csrf-token endpoint to issue tokens, and a middleware that validates x-csrf-token headers or _csrf body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with other weaknesses by automated attack tooling.
Introduction
The libProxy.js file is the heart of this Express.js application — it bootstraps the app and admin Express instances, registers rate limiters, and wires up all route handlers. But until this fix, there was a critical gap: none of those routes validated whether incoming state-changing requests actually originated from the application's own UI.
Semgrep flagged line 13 of libProxy.js with rule javascript.express.security.audit.express-check-csurf-middleware-usage, identifying that the Express app was initialized and routes registered without any CSRF protection in the middleware chain. This is a textbook example of a missing security control at the framework level — not a bug in a single function, but an absent layer of defense that exposed every POST, PUT, DELETE, and PATCH route simultaneously.
The Vulnerability Explained
What Was Missing
Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks a user's browser into sending an authenticated request to a target application the user is already logged into. The browser automatically attaches cookies and credentials, so the server has no way to distinguish a legitimate user action from a forged one — unless the server requires a secret token that only the legitimate UI can supply.
In the original libProxy.js, the Express application was set up like this (simplified):
// Before the fix — no CSRF protection anywhere
const app = express();
const admin = express();
// ...rate limiter, cors, jsonParser registered...
app.all('/', jsonParser, async (request, response) => {
response.setHeader('Content-Type', "application/json");
// ... handles all incoming requests without CSRF validation
});
The app.all('/') handler processes every HTTP method — including POST — with no check that the request came from a trusted source. Any webpage loaded in a victim's browser could silently fire a POST request to this endpoint.
A Concrete Attack Scenario
Imagine this application runs at https://proxy.internal.company.com and an employee uses it while logged in. An attacker crafts a malicious page:
<!-- attacker-controlled page -->
<form id="evil" action="https://proxy.internal.company.com/" method="POST">
<input name="action" value="delete_all_routes" />
</form>
<script>document.getElementById('evil').submit();</script>
When the employee visits the attacker's page, their browser automatically submits the form, including any session cookies. The Express server, lacking CSRF validation, processes the request as legitimate. The attacker never needed the user's password.
In a proxy application context — where routes likely control traffic forwarding, admin operations, or configuration — this is particularly dangerous. A forged request could redirect traffic, expose internal services, or modify security-sensitive configuration.
The Fix
What Changed in libProxy.js
The fix makes three targeted additions, all within the exports.start() function where the Express app is configured:
1. Import the csrf library and create a token factory
// Added at the top of libProxy.js
const csrf = require('csrf');
const csrfTokens = new csrf();
The csrf npm package (not to be confused with the deprecated csurf Express middleware) provides cryptographically secure token generation and verification without depending on sessions.
2. Expose a /csrf-token endpoint
const csrfSecret = csrfTokens.secretSync();
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: csrfTokens.create(csrfSecret) });
});
The application generates a single csrfSecret at startup. The /csrf-token GET endpoint uses that secret to create a signed token and returns it to the client. The frontend JavaScript must fetch this token before making any state-changing request.
3. Enforce token validation on all non-safe methods
app.use((req, res, next) => {
const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
if (safeMethods.includes(req.method)) return next();
const token = req.headers['x-csrf-token'] || (req.body && req.body._csrf);
if (!token || !csrfTokens.verify(csrfSecret, token)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
});
This middleware:
- Skips validation for safe, read-only methods (GET, HEAD, OPTIONS) per RFC 7231
- Accepts the token from either the x-csrf-token request header (for AJAX calls) or the _csrf body field (for form submissions)
- Rejects with 403 any request where the token is missing or fails csrfTokens.verify() against the server-side secret
Before vs. After
| Aspect | Before | After |
|---|---|---|
| CSRF middleware | ❌ None | ✅ Custom token validation middleware |
| Token issuance | ❌ None | ✅ GET /csrf-token endpoint |
| POST protection | ❌ Unprotected | ✅ Requires valid x-csrf-token or _csrf |
| Attack surface | All state-changing routes | Closed — forged requests rejected with 403 |
The middleware is registered before app.all('/'), so it intercepts every state-changing request before any route handler can process it.
Prevention & Best Practices
1. Always Register CSRF Protection Before Route Handlers
Middleware order in Express matters. CSRF validation must be registered before any route that processes POST, PUT, DELETE, or PATCH requests. Registering it after route handlers means it never runs for those routes.
2. Use the Double-Submit Cookie Pattern for Stateless APIs
For APIs that don't use sessions, the approach taken here — server-side secret + signed token — is appropriate. For session-based apps, the csurf middleware (now deprecated but still widely referenced) or a custom double-submit cookie implementation are alternatives.
3. Never Rely on CORS Alone
A common misconception: CORS prevents cross-origin reads but does not prevent cross-origin writes. Simple form POST requests bypass CORS entirely. CSRF tokens are the correct mitigation.
4. Rotate Secrets in Production
The fix uses csrfTokens.secretSync() called once at startup. In production, consider rotating the secret periodically or tying it to user sessions to limit the blast radius of a leaked token.
5. Validate on the Client Side Too
Frontend code should always fetch /csrf-token and include the token in requests:
// Example frontend usage
const { csrfToken } = await fetch('/csrf-token').then(r => r.json());
await fetch('/api/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ action: 'update' })
});
6. Scan for Missing CSRF Middleware Automatically
Semgrep's rule javascript.express.security.audit.express-check-csurf-middleware-usage detected this issue at libProxy.js:13. Integrate Semgrep into your CI/CD pipeline to catch this class of issue before code reaches production.
Relevant Standards
- OWASP Top 10: A01:2021 – Broken Access Control (CSRF falls under unauthorized actions)
- CWE-352: Cross-Site Request Forgery
- OWASP CSRF Prevention Cheat Sheet: Comprehensive guidance on token patterns, SameSite cookies, and defense-in-depth
Key Takeaways
libProxy.jshad zero CSRF protection: Every state-changing route — includingapp.all('/')— was reachable by a forged cross-origin request before this fix.- CORS ≠ CSRF protection: The app already used
cors, but that does not prevent forged form submissions or simple cross-origin POSTs. - Middleware order is security-critical in Express: The new CSRF middleware must be registered before route handlers, not after.
- The
csrfpackage enables stateless token validation: Unlike session-based approaches,csrfTokens.secretSync()+csrfTokens.verify()works cleanly in proxy/API architectures without requiring server-side session storage. - Static analysis caught what code review missed: Semgrep's
express-check-csurf-middleware-usagerule flagged the absence of CSRF middleware at the application initialization point (libProxy.js:13), demonstrating the value of automated scanning for missing controls — not just incorrect ones.
How Orbis AppSec Detected This
- Source: Any cross-origin HTTP request targeting the Express application's routes, including
app.all('/')registered inlibProxy.js - Sink: The unprotected
app.all('/')route handler atlibProxy.js(post-rate-limiter, pre-fix), which processedPOSTand other state-changing methods without token validation - Missing control: No CSRF token middleware or manual token verification existed anywhere in the Express middleware chain between app initialization and route registration
- CWE: CWE-352 — Cross-Site Request Forgery
- Fix: Added the
csrfnpm package, a/csrf-tokenissuance endpoint, and a pre-route middleware that rejects non-safe-method requests missing a validx-csrf-tokenheader or_csrfbody field with a403response
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 missing CSRF middleware is one of those vulnerabilities that's easy to overlook precisely because it's an absence rather than a mistake — there's no bad code to see, just a protection that was never added. In libProxy.js, every state-changing route was silently exposed to forged cross-origin requests until this fix landed.
The solution is clean and surgical: three additions to the existing startup sequence in exports.start() — a token factory, an issuance endpoint, and a validation middleware — close the entire attack surface without touching any business logic. The fix demonstrates that CSRF protection in Express doesn't require heavyweight session infrastructure; a shared server-side secret and a signed token are sufficient for stateless proxy architectures.
If your Express application doesn't have CSRF middleware in its stack, treat it as a high-severity issue. Automated tools like Semgrep can find it in seconds — but only if you're running them.