Introduction
The devboard/server/index.js file bootstraps the Devboard Express application — it wires up helmet for security headers, cors for cross-origin policy, express-rate-limit for abuse protection, and mounts the app's core routes: /api/auth, /api/tasks, and others. It does a lot of defensive setup. But a semgrep audit rule, javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage, flagged line 23 for a gap none of that other middleware covers: there was no CSRF protection at all.
That means every POST, PUT, and DELETE route in the application — think /api/data, /api/data/:id, task creation, task updates — trusted any request that arrived with a valid session cookie, regardless of where the request actually came from. A malicious webpage the victim happens to have open in another tab could fire off a fetch or auto-submitting form to Devboard's API, and the browser would happily attach the victim's cookies. If the developer working on this file assumed that helmet() and cors() were "enough" security, this vulnerability is a reminder that neither of those tools does anything to stop CSRF.
The Vulnerability Explained
Before the fix, the middleware stack in index.js looked like this:
app.use(express.json());
app.use(limiter);
// Routes
app.use("/api/auth", require("./routes/auth"));
app.use("/api/tasks", require("./routes/tasks"));
There's a rate limiter, JSON body parsing, and then the routes go straight into business logic. Nothing here checks who the request is really coming from — only that a session cookie exists. That's the essence of CSRF: the browser automatically attaches cookies to requests, even ones initiated by a third-party site, so the server can't tell a legitimate click from a forged one unless it explicitly checks for a token that a foreign origin couldn't have obtained.
Attack scenario: Imagine a Devboard user is logged in and, in another tab, visits https://malicious-site.com, which hosts this hidden auto-submitting form:
<form action="https://devboard-app.example/api/data/1" method="POST">
<input type="hidden" name="update" value="unauthorized" />
</form>
<script>document.forms[0].submit();</script>
Because the victim's browser automatically includes the Devboard session cookie with the request, the Express server in index.js sees what looks like a perfectly valid, authenticated POST /api/data/1 — and processes it. There was no middleware standing between express.json() and the route handlers to reject requests lacking a legitimate, session-bound CSRF token. The regression test included in this PR simulates exactly this: cross-origin POST/PUT/DELETE requests with an Origin header of https://malicious-site.com or https://attacker.com, expecting the server to reject them with a 401/403 or a CSRF-related error.
The Fix
The PR adds two dependencies — cookie-parser and csurf — and wires them into the middleware chain in devboard/server/index.js:
Before:
app.use(express.json());
app.use(limiter);
// Routes
app.use("/api/auth", require("./routes/auth"));
After:
const cookieParser = require("cookie-parser");
const csrf = require("csurf");
app.use(cookieParser());
app.use(express.json());
app.use(limiter);
// CSRF protection via csurf middleware (cookie-based)
app.use(csrf({ cookie: true }));
// Routes
app.use("/api/auth", require("./routes/auth"));
Here's why each change was necessary:
cookie-parser:csurfneeds to read and write a secret stored in a cookie in order to validate tokens across requests. Without a cookie parser,csurf({ cookie: true })has nothing to attach the CSRF secret to.csurf({ cookie: true }): This middleware generates a per-session CSRF secret (stored in a cookie) and requires that state-changing requests include a matching token (typically submitted via a form field or anX-CSRF-Tokenheader). Any request without a valid, matching token — like the forged form submission frommalicious-site.com— is rejected before it ever reaches/api/dataor/api/tasksroute handlers.- Placement matters: The middleware is registered after
cookieParser()andexpress.json()(so it can read cookies and parsed bodies) but before the route handlers, ensuring every route benefits from the check without each individual route needing its own logic. package.jsonupdate: Addingcookie-parserandcsurfas explicit dependencies makes the security control part of the reproducible build, not an implicit assumption.
The regression test bundled with the PR confirms the fix: cross-origin POST, PUT, and DELETE requests to /api/data and /api/data/1 without a valid CSRF token now return a 401/403 (or a CSRF-token error), instead of silently succeeding.
Prevention & Best Practices
- Always pair session cookies with a CSRF defense. If your app uses cookie-based sessions for any state-changing endpoint, you need CSRF protection —
SameSite=Lax/Strictcookies help but are not a complete substitute for token validation, especially for GET-triggered side effects or older browsers. - Register CSRF middleware globally, early, and consistently. Bolting CSRF checks onto only some routes is a common source of regressions; middleware applied once in
index.js, as done here, is far harder to accidentally bypass. - Use semgrep or similar SAST tooling in CI. The rule
express-check-csurf-middleware-usagethat caught this issue is designed to run automatically against any Express codebase — wiring it into CI would catch this class of gap before merge, not after a security audit. - Test CSRF behavior explicitly. The regression test in this PR — sending cross-origin requests with forged
Origin/Refererheaders and asserting a 401/403/404 — is a good template for verifying CSRF protection stays enforced as routes evolve. - Follow OWASP's CSRF Prevention Cheat Sheet. It covers synchronizer tokens (what
csurfimplements), double-submit cookies, andSameSitecookie strategies for cases where a token-based approach isn't practical.
Key Takeaways
- The
devboard/server/index.jsmiddleware stack hadhelmet,cors, and rate limiting — but none of those substitute for CSRF token validation on state-changing routes. csurf({ cookie: true })now sits between body parsing and route mounting, protecting/api/auth,/api/tasks, and all other mutating routes at once.cookie-parseris a required companion dependency forcsurf's cookie-based token storage — forgetting it silently breaks the protection.- The semgrep rule
express-check-csurf-middleware-usageis a reliable, low-noise signal for this exact gap in Express apps and should be part of any Node.js CI pipeline. - The included regression test — forging
Origin/Refererheaders onPOST/PUT/DELETE— is a reusable pattern for verifying CSRF protection doesn't regress as new routes are added.
How Orbis AppSec Detected This
- Source: Any cross-origin HTTP request (
POST,PUT,DELETE) sent to a Devboard API endpoint while the victim's browser holds a valid session cookie. - Sink: The route handlers mounted in
devboard/server/index.jsat line 23 and beyond (e.g.,/api/data,/api/data/:id,/api/tasks), which processed requests immediately after JSON body parsing. - Missing control: No CSRF token validation middleware (
csurf/csrf) existed anywhere in the middleware chain, so authenticity of the request's origin was never checked — only the presence of a session cookie. - CWE: CWE-352 (Cross-Site Request Forgery).
- Fix: Added
cookie-parserandcsurf({ cookie: true })middleware, registered right after body parsing and before route mounting, so every state-changing request must now present a valid, session-bound CSRF token.
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 is easy to overlook precisely because everything looks fine — the app is authenticated, the requests are well-formed JSON, and the session cookie checks out. But without a dedicated token-validation step, an Express app like Devboard has no way to distinguish a legitimate user action from a forged request fired by a hostile page the victim never intended to trust. Adding cookie-parser and csurf({ cookie: true }) to devboard/server/index.js closes that gap for every current and future route mounted after it, and the accompanying regression test ensures it stays closed. If your Express app handles session cookies and mutating routes, treat CSRF middleware as a non-negotiable baseline, not an afterthought.
References
- CWE-352: Cross-Site Request Forgery — https://cwe.mitre.org/data/definitions/352.html
- OWASP CSRF Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- csurf middleware documentation — https://github.com/expressjs/csurf
- cookie-parser documentation — https://github.com/expressjs/cookie-parser
- Semgrep rule reference — https://semgrep.dev/r?q=express-check-csurf-middleware-usage
- harden: add CSRF protection in index.js...