Back to Blog
high SEVERITY8 min read

How Missing CSRF Middleware happens in Express.js and how to fix it

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 ot

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

Answer Summary

This is a Cross-Site Request Forgery (CSRF) vulnerability (CWE-352) in an Express.js application (`libProxy.js`). Because no CSRF middleware was present, any state-changing route (POST, PUT, DELETE, PATCH) could be triggered by a malicious third-party website on behalf of an authenticated user. The fix adds the `csrf` npm package, exposes a `/csrf-token` endpoint to issue signed tokens, and enforces token validation via a custom middleware that rejects requests missing a valid `x-csrf-token` header or `_csrf` body parameter with a `403` response.

Vulnerability at a Glance

cweCWE-352
fixAdded csrf token library, /csrf-token issuance endpoint, and per-request token verification middleware
riskAttackers can forge authenticated requests from any origin, triggering state-changing operations on behalf of logged-in users
languageJavaScript (Node.js / Express.js)
root causeNo CSRF token validation existed for POST/PUT/DELETE/PATCH routes in libProxy.js
vulnerabilityMissing CSRF Middleware (Cross-Site Request Forgery)

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.js had zero CSRF protection: Every state-changing route — including app.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 csrf package 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-usage rule 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 in libProxy.js
  • Sink: The unprotected app.all('/') route handler at libProxy.js (post-rate-limiter, pre-fix), which processed POST and 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 csrf npm package, a /csrf-token issuance endpoint, and a pre-route middleware that rejects non-safe-method requests missing a valid x-csrf-token header or _csrf body field with a 403 response

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.


References

Frequently Asked Questions

What is a missing CSRF middleware vulnerability?

It means an Express.js application processes state-changing HTTP requests (POST, PUT, DELETE) without verifying that the request originated from the legitimate application UI, allowing attackers to forge requests on behalf of authenticated users.

How do you prevent CSRF vulnerabilities in Express.js?

Use a CSRF token library such as `csrf` or `csurf`, expose a token endpoint, and validate the token on every non-safe HTTP method (POST, PUT, DELETE, PATCH) via middleware before processing the request.

What CWE is CSRF?

CSRF is classified as CWE-352: Cross-Site Request Forgery.

Is CORS enough to prevent CSRF in Express.js?

No. CORS controls which origins can read responses, but it does not prevent browsers from sending cross-origin requests with cookies or credentials. A dedicated CSRF token mechanism is required.

Can static analysis detect missing CSRF middleware?

Yes. Semgrep's rule `javascript.express.security.audit.express-check-csurf-middleware-usage` scans Express.js apps for the absence of CSRF middleware and flags the entry point, as it did here at `libProxy.js:13`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #19

Related Articles

high

How CSRF bypass happens in React Router RSC mode and how to fix it

A high-severity CSRF bypass vulnerability (GHSA-qwww-vcr4-c8h2) in React Router's RSC (React Server Components) mode allowed attackers to execute actions before the framework returned a 400 response. This vulnerability affected React Router versions prior to 7.18.2 and 8.3.0, enabling cross-site request forgery attacks that could bypass standard CSRF protections in applications using RSC mode.

high

How CSRF vulnerability happens in JavaScript fetch() calls and how to fix it

A high-severity CSRF vulnerability was discovered in Moodle's VvvebJs page builder where POST requests to `saveReusableUrl` and `saveUrl` endpoints lacked CSRF token validation. Without proper sesskey inclusion, attackers could trick authenticated users into executing unauthorized page modifications. The fix adds Moodle's sesskey token to both client-side fetch requests and enforces server-side validation with `require_sesskey()`.

high

How CSRF protection gaps happen in Express.js applications and how to fix it

A high-severity security vulnerability was discovered in a React-Express booking application where the Express backend lacked CSRF middleware protection, while the frontend's coupon code input field in `Listing.jsx` allowed unrestricted user input. The fix implemented strict input validation using a regex pattern that whitelists only alphanumeric characters, hyphens, and underscores, preventing malicious payloads from reaching backend database operations.

high

How Regular Expression Denial of Service happens in JavaScript and how to fix it

CVE-2026-33671 is a Regular Expression Denial of Service (ReDoS) vulnerability in the picomatch glob-matching library, triggered by specially crafted extglob patterns that cause catastrophic regex backtracking. The fix upgrades picomatch to version 4.0.4 (with overrides pinning all transitive copies) in the client's dependency tree, eliminating the vulnerable regex evaluation path. Left unpatched, any code path that passes user-influenced glob patterns to picomatch could be weaponized to stall a

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.