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
Published August 31, 2026Reviewed 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.

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/Strict cookies 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-usage that 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/Referer headers 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 csurf implements), double-submit cookies, and SameSite cookie strategies for cases where a token-based approach isn't practical.

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.

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...

Frequently Asked Questions

What is Cross-Site Request Forgery (CSRF)?

CSRF is an attack where a malicious site tricks a logged-in victim's browser into sending an authenticated request (e.g., a form submit or fetch call) to a target application, causing unwanted state changes like data updates or deletions.

How do you prevent CSRF in Express.js?

Use a CSRF middleware such as `csurf` or `csrf` with cookie- or session-based tokens, validate the token on every state-changing request, and pair it with `SameSite` cookie attributes and origin checks for defense in depth.

What CWE is CSRF?

CSRF corresponds to CWE-352 (Cross-Site Request Forgery).

Is CORS configuration enough to prevent CSRF?

No. CORS controls which origins can read a response via JavaScript, but it does not stop a browser from sending a request with cookies attached — simple HTML forms and non-CORS requests can still trigger state changes without a CSRF token.

Can static analysis detect missing CSRF protection?

Yes. Rules like semgrep's `express-check-csurf-middleware-usage` scan Express apps for the absence of recognized CSRF middleware on routes that handle state-changing verbs, flagging the gap before it ships to production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #353

Related Articles

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.

high

How Sandboxed Iframe Popup Restriction Bypass happens in Electron and how to fix it

A high-severity flaw in Electron (CVE-2026-70608) allowed sandboxed iframes to bypass the `allow-popups` sandbox restriction through the internal OpenURL navigation path, letting malicious or compromised embedded content spawn unauthorized popup windows. The fix upgrades Electron from 40.10.6 to 41.10.3 (also patched in 42.0.1 and 39.8.10), closing the navigation-layer gap without requiring any application code changes.

critical

How Missing Authentication on DELETE Endpoints Happens in Python aiohttp and How to Fix It

A critical missing authentication vulnerability in `pz_minimax.py` allowed any network-connected user to delete stored MiniMax prompts via the `DELETE /pz_easyuse/minimax-prompts/{index}` endpoint without any access control. An attacker could enumerate sequential indices to wipe all user-created prompts from the shared JSON file. The fix restricts the DELETE endpoint to localhost-only requests by checking `request.remote` against loopback addresses.

critical

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.