Back to Blog
high SEVERITY6 min read

How javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage happens in Express.js and how to fix it

An Express.js application in `src/server.js` was missing CSRF (Cross-Site Request Forgery) protection middleware, leaving all state-changing endpoints vulnerable to forged requests from malicious sites. The fix introduces the `csrf` package to generate and validate tokens on non-GET requests, while exempting API-key-authenticated clients. This defensive hardening raises the bar against automated exploit chaining.

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

Answer Summary

This vulnerability is a missing CSRF middleware in an Express.js application (CWE-352: Cross-Site Request Forgery). Without CSRF token validation, attackers can trick authenticated users into making unintended state-changing requests. The fix adds the `csrf` npm package, creates a `/api/csrf-token` endpoint for clients to obtain tokens and secrets, and introduces a validation middleware that checks `X-CSRF-Token` and `X-CSRF-Secret` headers on all non-safe HTTP methods (POST, PUT, DELETE, PATCH).

Vulnerability at a Glance

cweCWE-352 (Cross-Site Request Forgery)
fixAdded the `csrf` package with token generation endpoint and validation middleware for state-changing requests
riskRemote attackers can forge state-changing requests on behalf of authenticated users
languageJavaScript (Node.js / Express)
root causeNo CSRF token generation or validation middleware was registered on the Express app
vulnerabilityMissing CSRF middleware in Express.js

Introduction

In src/server.js, the Express application was configured with bodyParser, cors, and several route handlers—but conspicuously absent was any form of CSRF protection. At line 8, where the app instance is created, Semgrep flagged that no CSRF middleware was detected anywhere in the application's middleware chain. This meant that every state-changing route—chat endpoints, model management, CLI interactions—was exposed to cross-site request forgery attacks from any malicious website a user might visit while authenticated.

The application registers multiple routers (modelsRouter, chatRouter, cliChatRouter, and others) that handle POST, PUT, and DELETE requests. Without CSRF validation, an attacker could craft a hidden form or JavaScript fetch on their site that submits requests to these endpoints, and the victim's browser would happily include session cookies, making the forged request indistinguishable from a legitimate one.

The Vulnerability Explained

What Was Missing

Here's the relevant section of src/server.js before the fix:

const express = require('express')
const bodyParser = require('body-parser')
const config = require('./config/index.js')
const cors = require('cors')
const { logger } = require('./utils/logger')
const { initSsxmodManager } = require('./utils/ssxmod-manager')
const DataPersistence = require('./utils/data-persistence')
const app = express()
const path = require('path')
const fs = require('fs')
// ... route registrations follow with NO CSRF middleware
app.use(bodyParser.json({ limit: '128mb' }))
app.use(bodyParser.urlencoded({ limit: '128mb', extended: true }))
app.use(cors())

Notice: bodyParser parses incoming request bodies, cors() sets access-control headers, but nothing validates that state-changing requests originated from the application itself. The cors() middleware alone is insufficient because:

  1. Simple POST requests with application/x-www-form-urlencoded content type don't trigger CORS preflight
  2. Even with CORS, the browser still sends the request—CORS only restricts reading the response
  3. Cookies (session tokens) are automatically attached by the browser regardless of CORS

Attack Scenario

Consider this application serves a chat interface. An attacker creates a page:

<form action="https://target-app.com/api/chat" method="POST">
  <input type="hidden" name="message" value="delete all conversations" />
</form>
<script>document.forms[0].submit();</script>

If a logged-in user visits this page, their browser submits the form to the chat endpoint with their session cookie. The server processes it as a legitimate request because there's no CSRF token to distinguish it from a real user action.

For this specific application—which handles AI model management and chat—an attacker could potentially:
- Send messages or commands on behalf of the user
- Modify model configurations
- Trigger resource-intensive operations (128MB body limit means large payloads are accepted)

The Fix

The fix introduces the csrf npm package and implements a two-part CSRF protection scheme: a token generation endpoint and a validation middleware.

Changes to package.json

"csrf": "^3.1.0",

The csrf package (not csurf, which is deprecated) provides low-level token generation and verification primitives.

Changes to src/server.js

New imports and initialization:

const Tokens = require('csrf')
// ...
const csrfTokens = new Tokens()

Token generation endpoint:

// CSRF token endpoint: browser clients GET a token tied to a per-request secret
app.get('/api/csrf-token', (req, res) => {
  const secret = csrfTokens.secretSync()
  const token = csrfTokens.create(secret)
  // Return both so the client can store the secret in sessionStorage and send
  // both back on state-changing requests via X-CSRF-Token and X-CSRF-Secret headers
  res.json({ csrfToken: token, csrfSecret: secret })
})

This endpoint generates a cryptographic secret and a token derived from it. The client stores both (e.g., in sessionStorage) and sends them back as custom headers on subsequent requests.

Validation middleware:

// CSRF validation middleware for state-changing browser requests
// API key clients (Authorization / x-api-key header) are exempt
const csrfProtect = (req, res, next) => {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next()
  if (req.headers['authorization'] || req.headers['x-api-key']) return next()
  const secret = req.headers['x-csrf-secret']
  const token = req.headers['x-csrf-token']
  if (!secret || !token || !csrfTokens.verify(secret, token)) {
    return res.status(403).json({ error: 'Invalid CSRF token' })
  }
  next()
}

Design Decisions

  1. Safe methods exempt: GET, HEAD, and OPTIONS are idempotent—they shouldn't cause state changes, so they're skipped.

  2. API key clients exempt: Requests with Authorization or x-api-key headers are already authenticated via non-cookie mechanisms. Since CSRF exploits cookie-based authentication, API-key clients aren't vulnerable.

  3. Custom headers required: The X-CSRF-Token and X-CSRF-Secret headers cannot be set by simple HTML forms, adding a layer of defense since cross-origin JavaScript requests with custom headers trigger CORS preflight.

Before vs. After

Aspect Before After
CSRF protection None Token-based validation via csrf package
State-changing requests Accepted without origin verification Require valid X-CSRF-Token + X-CSRF-Secret
API key clients No change Exempt (not cookie-based)
GET/HEAD/OPTIONS No change Explicitly skipped

Prevention & Best Practices

1. Always Include CSRF Protection for Cookie-Based Auth

If your Express app uses session cookies, CSRF middleware is non-negotiable. Add it early in your middleware chain, before route handlers.

2. Use the Synchronizer Token Pattern

The fix implements this correctly: generate a secret + token pair, give it to the client, and verify it on each state-changing request. This is the OWASP-recommended approach.

3. Consider Double-Submit Cookie as an Alternative

For stateless APIs, a double-submit cookie pattern (where the CSRF token is sent both as a cookie and a header) can work without server-side state.

4. Don't Rely on CORS Alone

CORS prevents reading responses cross-origin, but it doesn't prevent sending requests. Simple form POSTs bypass preflight entirely.

5. Automate Detection

Use Semgrep rules like express-check-csurf-middleware-usage in your CI pipeline to catch missing CSRF middleware before code reaches production.

6. SameSite Cookies as Defense-in-Depth

Set SameSite=Strict or SameSite=Lax on session cookies as an additional layer, but don't rely on it exclusively (older browsers may not support it).

Key Takeaways

  • The src/server.js Express app had cors() and bodyParser but zero CSRF validation—a common oversight when developers assume CORS provides full protection
  • The csrf package (v3.1.0) replaces the deprecated csurf middleware with a lower-level API that gives developers more control over token lifecycle
  • API-key-authenticated clients are correctly exempted because CSRF only exploits cookie-based authentication where the browser automatically attaches credentials
  • Custom headers (X-CSRF-Token, X-CSRF-Secret) provide defense-in-depth since they trigger CORS preflight on cross-origin requests, adding a second barrier
  • The 128MB body-parser limit combined with no CSRF protection meant attackers could trigger expensive operations via forged requests—the fix closes this amplification vector

How Orbis AppSec Detected This

  • Source: Browser-initiated HTTP requests to Express route handlers (any origin)
  • Sink: State-changing route handlers (POST /api/chat, POST /api/models, etc.) in src/server.js
  • Missing control: No CSRF token generation, no CSRF validation middleware registered on the Express app instance
  • CWE: CWE-352 (Cross-Site Request Forgery)
  • Fix: Added the csrf package with a /api/csrf-token generation endpoint and a csrfProtect middleware that validates X-CSRF-Token and X-CSRF-Secret headers on all non-safe, non-API-key requests

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

CSRF protection is one of those security controls that's easy to forget—especially in API-first applications where developers assume "it's just JSON, forms can't submit JSON." But as we've seen, the absence of CSRF middleware in src/server.js left every state-changing endpoint exploitable through forged cross-origin requests. The fix is elegant: leverage the csrf package's cryptographic token primitives, expose a generation endpoint, validate on state-changing requests, and exempt clients that don't use cookie-based auth. If your Express app serves browser clients with session cookies, make sure CSRF protection is in your middleware chain—not as an afterthought, but as a foundational security control.

References

Frequently Asked Questions

What is Cross-Site Request Forgery (CSRF)?

CSRF is an attack where a malicious website tricks a user's browser into making an unwanted request to a trusted site where the user is authenticated, exploiting the browser's automatic inclusion of cookies.

How do you prevent CSRF in Express.js?

Use a CSRF token library like `csrf` or `csurf`, generate unique tokens per session/request, embed them in forms or headers, and validate them server-side before processing state-changing requests.

What CWE is CSRF?

CSRF is classified as CWE-352: Cross-Site Request Forgery (CSRF), which describes the pattern of forging requests that exploit a user's authenticated session.

Is CORS enough to prevent CSRF?

No. CORS restricts which origins can read responses, but simple form submissions and some request types bypass CORS preflight checks entirely, so CSRF tokens are still required.

Can static analysis detect missing CSRF protection?

Yes. Tools like Semgrep can detect when an Express application lacks CSRF middleware registration, flagging it as a security audit finding before the code reaches production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #159

Related Articles

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

CVE-2026-59873 is a critical Denial of Service vulnerability in node-tar versions prior to 7.5.19, where a maliciously crafted gzip bomb can exhaust server resources when extracting archives. The fix upgrades the `tar` dependency from version 7.5.15 to 7.5.21 in `package-lock.json` and pins the version via an `overrides` block in `package.json`. Any application that processes user-supplied tar archives is at risk of resource exhaustion, making this an urgent upgrade.

high

How Client-Side Denial of Service happens in Node.js FTP clients and how to fix it

CVE-2026-44240 is a client-side Denial of Service vulnerability in the `basic-ftp` Node.js package (versions prior to 5.3.1) caused by improper handling of unterminated multiline FTP server responses. An attacker controlling an FTP server—or capable of intercepting FTP traffic—could send a malformed response that causes the client to hang indefinitely. Upgrading `basic-ftp` to 5.3.1 and adding a package override in `package.json` closes the attack surface entirely.

high

How javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage happens in Express.js and how to fix it

A publicly accessible Express.js API endpoint in `app/api/cameras.js` was missing CSRF protection, leaving state-changing requests (POST, PUT, DELETE, PATCH) vulnerable to cross-site request forgery attacks. The fix introduces Origin/Referer header validation middleware in `app/index.js` and removes a redundant Express instance from `cameras.js` that bypassed the application's middleware chain.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Unbounded Brace Expansion Happens in Node.js Dependencies and How to Fix It

A critical vulnerability in adm-zip (CVE-2026-39244) allowed attackers to craft malicious ZIP files that trigger unbounded brace expansion, causing excessive memory allocation and process crashes. The CortexKit project fixed this by upgrading adm-zip from 0.5.17 to 0.6.0, which implements bounds checking on expansion operations. This vulnerability demonstrates why dependency management and timely security updates are essential for production Node.js applications.