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:
- Attacker hosts malicious site
evil.com - Victim visits
evil.comwhile authenticated to the Topics server (or with the Topics API active) - Malicious JavaScript executes:
javascript fetch('http://localhost:3000/?bidder=attacker-controlled', { method: 'GET', credentials: 'include' }).then(r => r.json()).then(data => exfiltrate(data.topics)); - Sensitive browsing topic data exfiltrated — the
sec-browsing-topicsheader 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:
- Non-essential — purely demonstrative, not used by the core library
- High-risk — handles sensitive privacy data with no protection
- 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.jsfile 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...