Back to Blog
high SEVERITY6 min read

How Cross-Site Request Forgery (CSRF) happens in Express.js and how to fix it

A semgrep audit flagged `devboard/server/index.js` for lacking any CSRF middleware, meaning every state-changing route (`POST`, `PUT`, `DELETE` under `/api/*`) could be triggered by a forged cross-origin request riding on a victim's session cookie. The fix wires in `cookie-parser` and `csurf` right after body parsing, so every mutating request now requires a valid, per-session CSRF token before it reaches route handlers.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 31, 2026•Reviewed August 31, 2026

Answer Summary

This is a Cross-Site Request Forgery (CSRF) vulnerability, CWE-352, found in an Express.js/Node.js application at `devboard/server/index.js`. The app processed state-changing requests (`POST`/`PUT`/`DELETE` to `/api/*`) without any CSRF token validation middleware. The fix adds `cookie-parser` and `csurf({ cookie: true })` immediately after `express.json()`, forcing every mutating request to carry a valid CSRF token tied to the user's session before it reaches routes like `/api/auth`, `/api/tasks`, and `/api/data`.

Vulnerability at a Glance

cweCWE-352
fixAdded `cookie-parser` + `csurf({ cookie: true })` middleware before route mounting to enforce per-session CSRF tokens
riskAttackers can forge authenticated state-changing requests from a malicious page, using the victim's browser session
languageJavaScript (Node.js / Express)
root causeExpress app registered routes without any CSRF token validation middleware
vulnerabilityMissing CSRF Protection (Cross-Site Request Forgery)

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: csurf needs 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 an X-CSRF-Token header). Any request without a valid, matching token — like the forged form submission from malicious-site.com — is rejected before it ever reaches /api/data or /api/tasks route handlers.
  • Placement matters: The middleware is registered after cookieParser() and express.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.json update: Adding cookie-parser and csurf as 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.

Key Takeaways

  • The devboard/server/index.js middleware stack had helmet, 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-parser is a required companion dependency for csurf's cookie-based token storage — forgetting it silently breaks the protection.
  • The semgrep rule express-check-csurf-middleware-usage is 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/Referer headers on POST/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.js at 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-parser and csurf({ 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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #353

Related Articles

critical

Discord Bot Auto-Role CWE-269: Privilege Escalation via role.position

A Discord.js bot's auto-role feature allowed privilege escalation where any user with ManageRoles permission could add Administrator roles to automatic assignment lists, regardless of their own role hierarchy. The vulnerability existed because role.position validation checked only the bot's permissions, not the invoking user's role hierarchy relative to the target role.

high

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.