Back to Blog
high SEVERITY7 min read

How CSRF vulnerabilities happen in Express.js and how to fix them

A HIGH severity Cross-Site Request Forgery (CSRF) vulnerability was found in `integrationExamples/topics/topics-server.js`, an Express.js demo server that lacked any CSRF protection middleware. The file was deleted entirely after assessment determined it posed unnecessary risk to downstream consumers of this Node.js library.

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

This is a Cross-Site Request Forgery (CSRF) vulnerability (CWE-352) in an Express.js example server. The `topics-server.js` file at line 9 used `express.urlencoded()` and `express.json()` middleware without any CSRF token validation or `csurf` middleware protection. Attackers could forge requests to the `/` endpoint that reads sensitive Topics API data. The fix was complete removal of the vulnerable example file, as it served no production purpose and created supply chain risk for library consumers.

Vulnerability at a Glance

cweCWE-352 (Cross-Site Request Forgery)
fixComplete deletion of the vulnerable example file to eliminate attack surface
riskAttackers could forge cross-origin requests to extract sensitive browsing topic data from victims' browsers
languageJavaScript (Node.js/Express.js)
root causeMissing CSRF middleware (`csurf` or equivalent) on state-changing and data-exposing endpoints
vulnerabilityCross-Site Request Forgery (CSRF)

How CSRF Vulnerabilities Happen in Express.js and How to Fix Them


ANSWER_SUMMARY: This is a Cross-Site Request Forgery (CSRF) vulnerability (CWE-352) in an Express.js example server. The topics-server.js file at line 9 used express.urlencoded() and express.json() middleware without any CSRF token validation or csurf middleware protection. Attackers could forge requests to the / endpoint that reads sensitive Topics API data. The fix was complete removal of the vulnerable example file, as it served no production purpose and created supply chain risk for library consumers.


Introduction

In a Node.js library repository, we discovered a HIGH severity CSRF vulnerability in integrationExamples/topics/topics-server.js—a demo server intended to showcase Chrome's Topics API integration. The file at lines 9-16 configured Express with body-parsing middleware but completely omitted CSRF protection:

const app = express();
app.use(cors());
app.use(
  express.urlencoded({
    extended: true,
  })
);
app.use(express.json());

This pattern is dangerous because any endpoint that processes user data without CSRF tokens becomes vulnerable to cross-site request forgery. For a library distributed to thousands of downstream consumers, even "example" code can become an attack vector when developers copy-paste it into production.

The Vulnerability Explained

The Vulnerable Code Pattern

The problematic server implementation (lines 1-71 of topics-server.js) created an Express application with this critical security gap:

// Line 9-16: Body parsers enabled, ZERO CSRF protection
const app = express();
app.use(cors());  // Actually HELPS attackers by allowing cross-origin requests
app.use(
  express.urlencoded({
    extended: true,
  })
);
app.use(express.json());

// Line 38-52: Data-exposing endpoint with no validation
app.get('*', (req, res) => {
  res.setHeader('Observe-Browsing-Topics', '?1');

  const resData = {
    segment: {
      domain: req.hostname,
      topics: generateTopicArrayFromHeader(req.headers['sec-browsing-topics']),
      bidder: req.query['bidder'],  // User-controlled input
    },
    date: Date.now(),
  };

  res.json(resData);
});

Why This Is Exploitable

The app.use(cors()) configuration on line 10 is particularly dangerous here. CORS middleware with default settings allows any origin to make requests to this server. Combined with the missing CSRF protection, this creates a perfect storm:

  1. Attacker hosts malicious site evil.com
  2. Victim visits evil.com while authenticated to the Topics server (or with the Topics API active)
  3. Malicious JavaScript executes:
    javascript fetch('http://localhost:3000/?bidder=attacker-controlled', { method: 'GET', credentials: 'include' }).then(r => r.json()).then(data => exfiltrate(data.topics));
  4. Sensitive browsing topic data exfiltrated — the sec-browsing-topics header contains user's interest categories

The generateTopicArrayFromHeader() function (lines 55-71) parses this sensitive header data and returns it without any access control. Because there's no CSRF token validation, the browser happily includes cookies and makes the request.

Real-World Impact for Library Consumers

This vulnerability is especially insidious because:

  • It's example code — developers often copy integration examples verbatim
  • It handles sensitive data — Chrome's Topics API reveals user's browsing interests
  • CORS is misconfigured — the permissive setting suggests "this is safe for cross-origin use"
  • No authentication required — the endpoint exposes data to any requester

A developer copying this pattern into production would unknowingly create a data exfiltration endpoint.

The Fix

Assessment and Decision

After security review, the maintainers determined this file was:

  1. Non-essential — purely demonstrative, not used by the core library
  2. High-risk — handles sensitive privacy data with no protection
  3. Easily misused — copy-paste friendly but security-hostile

The Specific Change

The fix was complete deletion of integrationExamples/topics/topics-server.js:

diff --git a/integrationExamples/topics/topics-server.js b/integrationExamples/topics/topics-server.js
deleted file mode 100644
index 72f00ea2544..00000000000
--- a/integrationExamples/topics/topics-server.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// This is an example of a server-side endpoint that is utilizing the Topics API header functionality.
-// Note: This test endpoint requires the following to run: node.js, npm, express, cors
-
-const cors = require('cors');
-const express = require('express');
-
-const port = process.env.PORT || 3000;
-
-const app = express();
-app.use(cors());
-app.use(
-  express.urlencoded({
-    extended: true,
-  })
-);
-app.use(express.json());
-app.use(express.static('public'));
-app.set('port', port);
-
-const listener = app.listen(port, () => {
-  const host =
-    listener.address().address === '::'
-      ? 'http://localhost'
-      : 'http://' + listener.address().address;
-  // eslint-disable-next-line no-console
-  console.log(
-    `${__filename} is listening on ${host}:${listener.address().port}\n`
-  );
-});
-
-app.get('*', (req, res) => {
-  res.setHeader('Observe-Browsing-Topics', '?1');
-
-  const resData = {
-    segment: {
-      domain: req.hostname,
-      topics: generateTopicArrayFromHeader(req.headers['sec-browsing-topics']),
-      bidder: req.query['bidder'],
-    },
-    date: Date.now(),
-  };
-
-  res.json(resData);
-});
-
-const generateTopicArrayFromHeader = (topicString) => {
-  const result = [];
-  const topicArray = topicString.split(', ');
-  if (topicArray.length > 1) {
-    topicArray.pop();
-    topicArray.map((topic) => {
-      const topicId = topic.split(';')[0];
-      const versionsString = topic.split(';')[1].split('=')[1];
-      const [config, taxonomy, model] = versionsString.split(':');
-      const numTopicsWithSameVersions = topicId
-        .substring(1, topicId.length - 1)
-        .split(' ');
-
-      numTopicsWithSameVersions.map((tpId) => {
-        result.push({
-          topic: tpId,

Why Deletion Was the Right Fix

Rather than patching with csurf middleware, deletion was superior because:

Alternative Fix Why Deletion Won
Add csurf middleware Still leaves dangerous CORS + example code that will be copied
Restrict CORS origin Breaks the "integration example" purpose; still risky
Add authentication Overcomplicates a demo; developers will remove it
Delete file Eliminates all risk; examples can be documented instead

This follows the principle: "Code that doesn't exist can't be exploited."

Prevention & Best Practices

For Express.js Applications

If you must implement a similar endpoint, apply these layered defenses:

1. Add CSRF Middleware Properly

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// Apply to specific routes
app.post('/api/topics', csrfProtection, (req, res) => {
  // Process request
});

2. Configure CORS Restrictively

// NEVER use default cors() in production
const corsOptions = {
  origin: 'https://trusted-site.com',
  credentials: true,
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'X-CSRF-Token']
};
app.use(cors(corsOptions));

3. Validate and Sanitize All Inputs

The original code's req.query['bidder'] and req.headers['sec-browsing-topics'] were used without validation. Implement strict parsing:

const { body, validationResult } = require('express-validator');

app.post('/topics', 
  body('bidder').isAlphanumeric().isLength({ max: 50 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process validated data
  }
);

Detection Tools

Tool Rule/Feature Purpose
Semgrep express-check-csurf-middleware-usage Detects missing CSRF protection
ESLint security/detect-object-injection Finds injection risks
Node.js Security WG eslint-plugin-security General security linting

Standards References

  • OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
  • OWASP Express.js Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html
  • CWE-352: Cross-Site Request Forgery (CSRF)

Key Takeaways

  • Example code is attack surface: The topics-server.js file was "just documentation" but created real supply chain risk for library consumers who might copy it
  • CORS is not a security control: The app.use(cors()) pattern actively enabled cross-origin attacks rather than preventing them
  • Deletion beats patching: When vulnerable code serves no critical purpose, removing it eliminates entire classes of future vulnerabilities
  • Sensitive APIs need defense in depth: The Topics API handles privacy-sensitive data; any endpoint touching it requires CSRF tokens, strict origin validation, and input sanitization
  • Static analysis catches architectural flaws: Semgrep's rule detected the missing middleware pattern before any reported exploitation

How Orbis AppSec Detected This

Orbis AppSec's automated security analysis identified this vulnerability through precise data flow tracking:

Component Details
Source HTTP request parameters req.query['bidder'] and req.headers['sec-browsing-topics'] in topics-server.js:48-49
Sink JSON response construction res.json(resData) at topics-server.js:52 exposing sensitive topic data
Missing control No csurf middleware, no CSRF token validation, and permissive CORS configuration
CWE CWE-352: Cross-Site Request Forgery (CSRF)
Fix Complete removal of integrationExamples/topics/topics-server.js to eliminate the vulnerable code pattern from the repository

The detection leveraged Semgrep's express-check-csurf-middleware-usage rule, which specifically flags Express applications that parse request bodies without CSRF protection. This architectural pattern—body parsers without tokens—creates an exploit primitive that automated attack tools can weaponize when combined with other weaknesses.

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

The topics-server.js CSRF vulnerability demonstrates how even "harmless" example code can become a supply chain liability. The combination of Express body-parsing middleware, permissive CORS, and sensitive data exposure created a vulnerability that required no authentication to exploit.

For library maintainers, this case reinforces: audit your examples with the same rigor as production code. For developers using third-party libraries: never copy integration examples without security review. The fix—complete deletion—shows that sometimes the most secure code is the code you don't ship.

Secure your Express.js applications with proper CSRF middleware, restrictive CORS policies, and regular static analysis. Your downstream users depend on it.


References

  • CWE-352: Cross-Site Request Forgery (CSRF): 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
  • Express.js Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html
  • csurf middleware documentation: https://www.npmjs.com/package/csurf
  • Semgrep rule: express-check-csurf-middleware-usage: https://semgrep.dev/r?q=javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage
  • harden: add CSRF protection in topics-server.js...

Frequently Asked Questions

What is CSRF in Express.js?

CSRF (Cross-Site Request Forgery) is an attack where malicious sites trick users' browsers into making unintended requests to your Express.js server, exploiting the browser's automatic cookie/session handling.

How do you prevent CSRF in Express.js?

Use the `csurf` middleware to generate and validate CSRF tokens on all state-changing routes, or implement Double Submit Cookie pattern with `cookie-parser` and custom token validation.

What CWE is CSRF?

CWE-352: Cross-Site Request Forgery (CSRF)

Is CORS enough to prevent CSRF?

No. CORS (`cors()` middleware) only controls cross-origin resource sharing—it does NOT prevent CSRF attacks. The vulnerable code actually used `app.use(cors())` which made CSRF attacks easier, not harder.

Can static analysis detect missing CSRF protection?

Yes. Semgrep's `express-check-csurf-middleware-usage` rule specifically flags Express applications that use body-parsing middleware without CSRF protection, as seen in this detection.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15440

Related Articles

critical

How a vulnerable websocket-driver dependency happens in Node.js lockfiles and how to fix it

A Trivy scan flagged `websocket-driver@0.7.4` in this repository's `bun.lock` as affected by CVE-2026-54466, a critical issue in a WebSocket protocol handler that parses untrusted HTTP upgrade requests and frame data. The fix upgrades the package to `0.7.5` and adds an explicit `websocket-driver` entry to the lockfile's override block so every transitive consumer — webpack-dev-server, sockjs, faye-websocket — resolves to the patched build instead of the pinned vulnerable one.

high

How Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.

high

How Exponential-Time Complexity Causes Denial of Service in brace-expansion and How to Fix It

A critical vulnerability in brace-expansion versions 1.1.13 and earlier allowed attackers to cause denial of service through crafted brace pattern inputs. The fix upgrades to patched versions 1.1.16, 2.1.2, and 5.0.7, eliminating the exponential-time complexity that made exploitation possible.

high

How unrestricted file upload via extension-only validation happens in Deno/JavaScript and how to fix it

The review image upload handler in this Deno-based app trusted the client-supplied filename extension to decide whether a file was a "safe" image, without ever inspecting the actual file bytes. The fix adds magic-byte signature verification for PNG, JPEG, GIF, and WEBP formats before the file is written to disk, closing the door on disguised executables and malicious payloads.

high

How Missing Dependabot Cooldown Periods Enable Supply Chain Attacks in CI/CD Pipelines and How to Fix Them

We fixed a high-severity supply chain security gap in `.github/dependabot.yml` where missing cooldown periods allowed immediate adoption of newly published packages. The fix adds `cooldown: default-days: 7` to all package ecosystems, creating a critical security buffer against typosquatting and malicious dependency attacks.

high

How Dependabot Missing Cooldown Vulnerability Happens in GitHub Actions and How to Fix It

Dependabot configurations without cooldown periods can automatically propose updates from newly published packages within hours—potentially including malicious or unstable versions. This vulnerability in `.github/dependabot.yml` was fixed by adding a `cooldown` block with `default-days: 7` to delay updates and allow time for community vetting.