Introduction
In the tower_game/index.js file — a Node.js Express server that serves a browser-based tower game — we discovered a high-severity security gap: the application was configured with static file serving but completely lacked CSRF (Cross-Site Request Forgery) protection. Starting at line 5, the Express server was initialized and immediately began serving assets without any middleware to validate the origin of state-changing requests:
const server = express()
const host = 'http://localhost:8082'
server.use('/assets', express.static(path.resolve(__dirname, './assets')))
server.use('/dist', express.static(path.resolve(__dirname, './dist')))
This pattern — creating an Express app and adding routes without CSRF middleware — is flagged by Semgrep's express-check-csurf-middleware-usage rule because it leaves every POST, PUT, and DELETE endpoint vulnerable to cross-site forged requests.
The Vulnerability Explained
What's Actually Happening
Cross-Site Request Forgery exploits the trust that a web application has in the user's browser. When a user is authenticated with the tower game server (via session cookies, for example), their browser automatically attaches those cookies to every request sent to localhost:8082 — even if that request originates from a completely different website.
The vulnerable code in tower_game/index.js looked like this:
const express = require('express')
const path = require('path')
const opn = require('opn')
const server = express()
const host = 'http://localhost:8082'
server.use('/assets', express.static(path.resolve(__dirname, './assets')))
server.use('/dist', express.static(path.resolve(__dirname, './dist')))
There is zero validation that incoming requests actually originated from the tower game's own pages. No token checking, no origin validation, no CSRF middleware of any kind.
Attack Scenario Specific to This Application
Imagine the tower game has a score submission endpoint (as suggested by the game's nature). An attacker could create a malicious webpage like this:
<!-- attacker's page: evil-gaming-site.com -->
<form action="http://localhost:8082/api/score" method="POST" id="exploit">
<input type="hidden" name="score" value="999999" />
</form>
<script>document.getElementById('exploit').submit();</script>
If a player who has the tower game open in another tab visits this page, the form auto-submits a fraudulent high score. The browser dutifully sends along any cookies associated with localhost:8082, and the server has no way to distinguish this forged request from a legitimate one.
While a local game server might seem low-risk, this pattern becomes critical when:
- The game is deployed to a public server with user accounts
- The server handles any authentication or user data
- The same codebase pattern is copied into production applications
Why Static Analysis Flagged This
Semgrep's rule express-check-csurf-middleware-usage performs a structural analysis of Express application setup. It looks for express() instantiation followed by route/middleware registration and checks whether any CSRF middleware (csurf, csrf, or equivalent) is registered. When none is found, it raises a HIGH severity finding because the absence of CSRF protection is a well-known, exploitable weakness.
The Fix
The fix adds four lines to tower_game/index.js that establish cookie-based CSRF protection:
Before (Vulnerable)
const express = require('express')
const path = require('path')
const opn = require('opn')
const server = express()
const host = 'http://localhost:8082'
server.use('/assets', express.static(path.resolve(__dirname, './assets')))
After (Secured)
const express = require('express')
const path = require('path')
const opn = require('opn')
const cookieParser = require('cookie-parser')
const csrf = require('csurf')
const server = express()
const host = 'http://localhost:8082'
server.use(cookieParser())
server.use(csrf({ cookie: true }))
server.use('/assets', express.static(path.resolve(__dirname, './assets')))
How Each Change Works
-
const cookieParser = require('cookie-parser')— Imports the cookie-parser middleware, which is required bycsurfwhen using cookie-based token storage. It parses theCookieheader and populatesreq.cookies. -
const csrf = require('csurf')— Imports thecsurfmiddleware that implements the synchronizer token pattern for CSRF protection. -
server.use(cookieParser())— Registers cookie parsing globally, ensuringcsurfcan read the CSRF secret from the_csrfcookie on incoming requests. -
server.use(csrf({ cookie: true }))— Registers CSRF protection globally with cookie-based storage. This means:
- A_csrfcookie containing a secret is set on the client
- Every state-changing request (POST, PUT, DELETE, PATCH) must include a valid CSRF token derived from that secret
- The token can be sent via the_csrfbody field,csrf-tokenheader, orx-csrf-tokenheader
- Requests without a valid token receive a403 Forbiddenresponse
The middleware is registered before the static file routes, ensuring all subsequently defined routes inherit CSRF protection. Static file serving (GET requests) is unaffected since csurf only validates non-safe HTTP methods.
Prevention & Best Practices
1. Always Add CSRF Middleware Early in the Stack
Register CSRF protection immediately after session/cookie middleware and before any route handlers:
server.use(cookieParser())
server.use(session({ /* ... */ }))
server.use(csrf({ cookie: true }))
// Routes go here
2. Include CSRF Tokens in Your Frontend
For the tower game's frontend to work with CSRF protection, templates or API responses must provide the token:
server.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
The frontend then includes this token in subsequent requests:
fetch('/api/score', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ score })
});
3. Layer Your Defenses
- Set
SameSite=StrictorSameSite=Laxon session cookies - Validate the
OriginandRefererheaders as an additional check - Use
helmetmiddleware for security headers
4. Use Static Analysis in CI/CD
Run Semgrep with the express-check-csurf-middleware-usage rule in your CI pipeline to catch missing CSRF protection before code reaches production.
5. Note on csurf Deprecation
The csurf package is deprecated. For new projects, consider alternatives like csrf-csrf or lusitania. The underlying pattern (synchronizer token) remains the same — only the package implementation differs.
Key Takeaways
- The
tower_game/index.jsExpress server had no CSRF middleware, meaning any cross-origin POST request would be accepted without validation — a textbook CWE-352 vulnerability. - Cookie-based CSRF protection requires both
cookie-parserandcsurf— forgettingcookieParser()beforecsrf({ cookie: true })will cause runtime errors. - Middleware ordering matters:
csrf()must be registered before route handlers but after cookie/session parsing to function correctly. - Static file routes (GET) are unaffected by csurf — the middleware only validates state-changing HTTP methods, so the game's asset serving continues to work without tokens.
- Semgrep's structural analysis caught this at the application architecture level — not a bug in a specific route, but a missing security layer across the entire application.
How Orbis AppSec Detected This
- Source: Any incoming HTTP request to the Express server at
localhost:8082, particularly state-changing methods (POST, PUT, DELETE) - Sink: All route handlers registered on the
serverExpress instance intower_game/index.js:5that process state-changing requests - Missing control: No CSRF middleware (
csurf,csrf, or equivalent token validation) was registered on the Express application - CWE: CWE-352 (Cross-Site Request Forgery)
- Fix: Added
cookieParser()andcsrf({ cookie: true })as global middleware on the Express server, requiring valid CSRF tokens for all non-safe HTTP methods
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
Missing CSRF protection is one of those vulnerabilities that's easy to overlook — especially in game servers or internal tools where security isn't the primary focus. But as the OWASP Top 10 consistently reminds us, CSRF remains a real threat that can escalate from "harmless game hack" to "account takeover" when applications grow.
The fix for tower_game/index.js demonstrates how minimal the effort is: four lines of code — two imports and two middleware registrations — close an entire class of attacks. If you're building Express applications, make CSRF middleware as automatic as express.json() in your server setup.