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:
- Simple POST requests with
application/x-www-form-urlencodedcontent type don't trigger CORS preflight - Even with CORS, the browser still sends the request—CORS only restricts reading the response
- 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
-
Safe methods exempt: GET, HEAD, and OPTIONS are idempotent—they shouldn't cause state changes, so they're skipped.
-
API key clients exempt: Requests with
Authorizationorx-api-keyheaders are already authenticated via non-cookie mechanisms. Since CSRF exploits cookie-based authentication, API-key clients aren't vulnerable. -
Custom headers required: The
X-CSRF-TokenandX-CSRF-Secretheaders 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.jsExpress app hadcors()andbodyParserbut zero CSRF validation—a common oversight when developers assume CORS provides full protection - The
csrfpackage (v3.1.0) replaces the deprecatedcsurfmiddleware 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.) insrc/server.js - Missing control: No CSRF token generation, no CSRF validation middleware registered on the Express
appinstance - CWE: CWE-352 (Cross-Site Request Forgery)
- Fix: Added the
csrfpackage with a/api/csrf-tokengeneration endpoint and acsrfProtectmiddleware that validatesX-CSRF-TokenandX-CSRF-Secretheaders 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.