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

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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15440

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.