Back to Blog
high SEVERITY6 min read

How javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage happens in Express.js and how to fix it

A publicly accessible Express.js API endpoint in `app/api/cameras.js` was missing CSRF protection, leaving state-changing requests (POST, PUT, DELETE, PATCH) vulnerable to cross-site request forgery attacks. The fix introduces Origin/Referer header validation middleware in `app/index.js` and removes a redundant Express instance from `cameras.js` that bypassed the application's middleware chain.

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

Answer Summary

Cross-Site Request Forgery (CSRF) vulnerability (CWE-352) was detected in an Express.js application where `app/api/cameras.js` instantiated its own `express()` app without CSRF middleware, bypassing any protections on the main app. The fix removes the isolated Express instance in `cameras.js` and adds Origin/Referer header validation middleware to the main `app/index.js`, ensuring all state-changing HTTP methods are protected against cross-origin forged requests.

Vulnerability at a Glance

cweCWE-352
fixRemoved redundant Express app and added Origin/Referer validation middleware to main app
riskAttackers can forge state-changing requests to the cameras API from malicious websites
languageJavaScript (Node.js/Express)
root causeSeparate Express instance in cameras.js bypassed application-level middleware
vulnerabilityMissing CSRF Middleware

Introduction

The app/api/cameras.js file in this application handles camera-related API operations, but a critical architectural flaw created a CSRF vulnerability: at line 4, the file instantiated its own independent Express application with const app = express(). This separate Express instance operated outside the main application's middleware chain defined in app/index.js, meaning any CSRF protections applied to the main app would never reach requests handled by this module.

This is a subtle but dangerous pattern. Even if a developer added CSRF middleware to the main application, the cameras API would remain completely unprotected because it was running on its own isolated Express instance without any such middleware configured.

The Vulnerability Explained

The Problematic Code

In the original app/api/cameras.js, the file created its own Express application:

const express = require('express');
const httpRequest = require('../lib/http');
const bodyParser = require('body-parser');
const app = express();
var SelfReloadJSON = require('self-reload-json');
const appRoot = require('app-root-path');
var settings = new SelfReloadJSON(appRoot + '/data/settings.json');

The key issue is lines 1, 3, and 4: importing express and body-parser, then creating a standalone const app = express(). This module-level Express instance would handle routes independently, completely bypassing any middleware (including CSRF protection) configured on the main application in app/index.js.

How Could This Be Exploited?

Consider this attack scenario:

  1. A user is authenticated to the camera management system and has an active session.
  2. The user visits a malicious website while still logged in.
  3. The malicious site contains a hidden form or JavaScript that submits a POST request to the cameras API endpoint:
<form action="https://target-app.com/api/cameras" method="POST">
    <input type="hidden" name="url" value="http://attacker-controlled-feed.com/spy" />
    <input type="hidden" name="name" value="Lobby Camera" />
</form>
<script>document.forms[0].submit();</script>
  1. The browser automatically includes the user's session cookies with the request.
  2. Because there's no CSRF validation, the cameras API processes the request as legitimate, potentially allowing an attacker to:
    - Add malicious camera feeds pointing to attacker-controlled streams
    - Delete existing camera configurations
    - Modify camera settings to disable monitoring

Since the PR description confirms "This API endpoint appears to be publicly accessible" and "This is a web service - vulnerabilities in request handlers are directly exploitable by remote attackers," the attack surface is real and immediately exploitable.

The Fix

The fix addresses this vulnerability through changes in two files, each serving a distinct purpose:

Change 1: Remove the Redundant Express Instance (app/api/cameras.js)

Before:

const express = require('express');
const httpRequest = require('../lib/http');
const bodyParser = require('body-parser');
const app = express();
var SelfReloadJSON = require('self-reload-json');

After:

const httpRequest = require('../lib/http');
var SelfReloadJSON = require('self-reload-json');

The imports for express and body-parser are removed, along with the standalone const app = express() declaration. This ensures that the cameras module no longer operates as an isolated Express application. Instead, it will use routes registered on the main application, inheriting all middleware in the chain.

Change 2: Add CSRF Protection Middleware (app/index.js)

Before: No CSRF protection existed in the middleware chain.

After:

// CSRF protection: verify Origin/Referer header matches Host for state-changing requests
app.use(function csrfProtection(req, res, next) {
    if (['POST', 'PUT', 'DELETE', 'PATCH'].indexOf(req.method) !== -1) {
        var origin = req.headers.origin || req.headers.referer;
        var host = req.headers.host;
        if (origin && host && origin.indexOf(host) === -1) {
            return res.status(403).json({ error: 'CSRF check failed' });
        }
    }
    next();
});

This middleware:
1. Targets only state-changing methods — GET and HEAD requests pass through unaffected since they should be idempotent.
2. Validates the Origin or Referer header — It checks whether the request's origin matches the application's host. Cross-origin forged requests will have a mismatched origin.
3. Returns 403 on mismatch — Requests failing validation receive a clear { error: 'CSRF check failed' } response.
4. Allows requests without Origin/Referer — This handles same-origin requests where browsers may not send these headers (e.g., direct API calls), avoiding false positives.

Why Both Changes Are Necessary

Removing the redundant Express instance ensures that cameras.js routes are served through the main app, which now has CSRF middleware. Without removing the standalone app = express(), the cameras module would continue to bypass any middleware added to app/index.js.

Key Takeaways

  • A standalone const app = express() in cameras.js created an isolated middleware chain, meaning CSRF protections on the main app were completely bypassed for camera API routes.
  • Origin/Referer header validation is an effective CSRF defense that doesn't require token management or client-side changes, making it ideal for API-first applications.
  • The fix targets only state-changing methods (POST, PUT, DELETE, PATCH), preserving backward compatibility for safe read operations.
  • Module-level Express instances are a common architectural anti-pattern that fragments security controls — always use express.Router() for sub-modules.
  • Two-file fixes are sometimes necessary: removing the bypass path (cameras.js) and adding the protection (index.js) together close the vulnerability completely.

How Orbis AppSec Detected This

  • Source: Incoming HTTP requests to the cameras API endpoints (state-changing methods like POST/PUT/DELETE)
  • Sink: Route handlers in app/api/cameras.js that process requests without CSRF validation
  • Missing control: No CSRF middleware (like csurf) was detected in the Express middleware chain, and the standalone express() instance in cameras.js:4 bypassed any application-level protections
  • CWE: CWE-352 (Cross-Site Request Forgery)
  • Fix: Removed the isolated Express instance from cameras.js and added Origin/Referer header validation middleware to the main application in app/index.js

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

This vulnerability demonstrates how architectural decisions — specifically creating a standalone Express instance in a sub-module — can silently undermine security controls. The redundant const app = express() in cameras.js meant that even if CSRF middleware existed on the main application, the cameras API remained completely exposed. The fix elegantly addresses both the symptom (missing CSRF protection) and the root cause (isolated Express instance) by consolidating the middleware chain and adding Origin/Referer validation for all state-changing requests.

When building Express.js applications, always use express.Router() for modular routes and apply security middleware at the application level to ensure consistent protection across all endpoints.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #33

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

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.