Back to Blog
high SEVERITY8 min read

How Missing Authentication Happens in Express.js APIs and How to Fix It

Four Express.js API endpoints in `index.js` — `/api/config`, `/api/subscriptions`, `/api/sites`, and `/api/refresh` — were fully accessible without any authentication, allowing any remote attacker to retrieve sensitive application data. The fix introduces both an API key authentication middleware and CSRF token protection, ensuring only authorized clients can interact with these endpoints. This is a common but critical oversight in Node.js web services that can expose configuration secrets and s

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This vulnerability is a missing authentication control (CWE-306) in an Express.js application where four API endpoints (`/api/config`, `/api/subscriptions`, `/api/sites`, `/api/refresh`) in `index.js` had no credential checks whatsoever. Any unauthenticated HTTP request could retrieve sensitive application data. The fix adds an `apiAuth` middleware that validates an `x-api-key` header against an environment variable, plus a `csrfProtect` middleware using the `csrf` package to block cross-site request forgery on state-changing requests. Both middlewares are applied globally to the `/api` route prefix.

Vulnerability at a Glance

cweCWE-306
fixAdded `apiAuth` middleware (API key check) and `csrfProtect` middleware (CSRF token validation) applied to all `/api` routes
riskAny unauthenticated remote attacker can access sensitive configuration, subscription, and site data
languageJavaScript (Node.js)
root causeExpress.js route handlers for `/api/*` endpoints registered with no authentication middleware
vulnerabilityMissing Authentication for Critical Function

The Problem With Unguarded API Routes

The index.js file in this Express.js application acts as the central routing hub, exposing endpoints that serve configuration data, subscription links, and site metadata. But when the Orbis AppSec scanner analyzed the file, it found something alarming: every single one of these endpoints — /api/config, /api/subscriptions, /api/sites, and /api/refresh — was reachable by anyone on the internet, no credentials required.

This isn't a subtle logic flaw or a tricky edge case. It's a straightforward omission: the route handlers were registered with app.get('/api/config', (req, res) => { ... }) and nothing else. No middleware. No token check. No session validation. Just open doors.

For developers building internal tools or prototypes, this pattern is easy to fall into — you add routes quickly, plan to "add auth later," and later never comes. In production, the consequences can be severe.


The Vulnerability Explained

What Was Actually Exposed

Looking at the diff, the original route registrations were clean and simple — and completely unprotected:

// BEFORE — no authentication whatsoever
app.get('/api/config', (req, res) => {
  try {
    const publicConfig = {
      sites: config.sites.map(site => ({
        // ... site configuration data
      }))
    };
    // ...
  }
});

app.get('/api/subscriptions', (req, res) => {
  try {
    const subscriptionsData = {};
    // reads JSON files from dataDir and returns them
    sites.forEach(site => {
      const siteData = fs.readJsonSync(path.join(dataDir, site));
      // ...
    });
  }
});

There is no authMiddleware, no passport.authenticate(), no req.session check — nothing between the incoming HTTP request and the response handler.

The Attack Is Trivially Simple

An attacker doesn't need to exploit a buffer overflow or craft a malicious payload. They just send a GET request:

curl http://target-host:3000/api/config
curl http://target-host:3000/api/subscriptions

That's it. Within milliseconds, they receive:
- /api/config: Application configuration including site URLs, schedule settings, and structural metadata
- /api/subscriptions: Full subscription link data read from JSON files in dataDir
- /api/sites: Site enumeration data
- /api/refresh: Potentially triggers a scraper refresh cycle

The /api/subscriptions endpoint is particularly sensitive — it reads .json files from a data directory using fs.readJsonSync() and returns their contents. Depending on what those files contain, this could expose user data, API tokens stored in config files, or internal service URLs.

Why This Is CWE-306

This vulnerability maps directly to CWE-306: Missing Authentication for Critical Function. The application performs sensitive operations (reading config, returning subscription data, triggering scraper jobs) without establishing who is making the request. There's no identity check at any layer of the request pipeline.

The scanner flagged this as CRITICAL because:
1. It's directly exploitable with zero prerequisites
2. The application is a web service — remote attackers can reach it
3. The data returned includes application internals that could enable further attacks


The Fix

The fix introduces two distinct security controls, applied at different layers of the request pipeline.

Control 1: API Key Authentication Middleware

// AFTER — apiAuth middleware added
const apiAuth = (req, res, next) => {
  const apiKey = process.env.API_KEY;
  if (!apiKey) return next(); // graceful degradation if not configured
  const token = req.headers['x-api-key'] || req.query.api_key;
  if (token !== apiKey) return res.status(401).json({ error: 'Unauthorized' });
  next();
};

This middleware:
- Reads the expected key from process.env.API_KEY (never hardcoded)
- Checks both the x-api-key header and api_key query parameter for flexibility
- Returns 401 Unauthorized if the token doesn't match
- Gracefully skips the check if API_KEY isn't set (useful during local development)

The middleware is then applied directly to each sensitive route:

// BEFORE
app.get('/api/config', (req, res) => { ... });
app.get('/api/subscriptions', (req, res) => { ... });

// AFTER
app.get('/api/config', apiAuth, (req, res) => { ... });
app.get('/api/subscriptions', apiAuth, (req, res) => { ... });

By inserting apiAuth as the second argument to app.get(), Express will call it before the route handler. If apiAuth calls res.status(401).json(...), the route handler never executes.

Control 2: CSRF Token Protection

The fix also adds CSRF protection using the csrf npm package, which guards against cross-site request forgery on state-changing requests:

const csrfLib = new Csrf();
const csrfSecret = crypto.randomBytes(18).toString('base64');

const csrfProtect = (req, res, next) => {
  if (req.method === 'GET') return next(); // GETs are safe
  const token = req.headers['x-csrf-token'];
  if (!token || !csrfLib.verify(csrfSecret, token)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
};

app.get('/api/csrf-token', (req, res) => {
  res.json({ csrfToken: csrfLib.create(csrfSecret) });
});

app.use('/api', csrfProtect); // applied to ALL /api routes

A few design decisions worth noting:
- crypto.randomBytes(18).toString('base64') generates a cryptographically random secret at startup — not a hardcoded string
- CSRF checks are skipped for GET requests (which should be idempotent and non-state-changing)
- The /api/csrf-token endpoint lets legitimate frontend clients obtain a token before making POST/PUT/DELETE requests
- app.use('/api', csrfProtect) applies the middleware to every sub-route under /api in one line

The Path Traversal Bonus Fix

The diff also reveals a secondary fix in the /api/subscriptions handler:

// BEFORE — potential path traversal
const siteData = fs.readJsonSync(path.join(dataDir, site));
const siteName = site.replace('.json', '');

// AFTER — sanitized filename
const safeFile = path.basename(site);

By applying path.basename() before passing the filename to path.join(), the fix strips any directory traversal sequences like ../../etc/passwd that might appear in the site variable. This is a defense-in-depth improvement on top of the authentication fix.


Key Takeaways

  • /api/config, /api/subscriptions, /api/sites, and /api/refresh in index.js were all publicly accessible — a single omission of middleware exposed the entire API surface
  • The apiAuth middleware pattern (check process.env.API_KEY against req.headers['x-api-key']) is a minimal, effective guard for internal APIs that don't need full OAuth/JWT infrastructure
  • app.use('/api', csrfProtect) is more reliable than per-route CSRF — applying middleware at the prefix level means new routes are protected automatically
  • path.basename() is a one-line fix for path traversal in file-reading routes — always sanitize filenames derived from request data before passing them to fs functions
  • Graceful degradation (if (!apiKey) return next()) makes the auth middleware developer-friendly without sacrificing production security — just set API_KEY in your deployment environment

How Orbis AppSec Detected This

  • Source: Incoming HTTP requests to /api/config, /api/subscriptions, /api/sites, and /api/refresh — no identity information required
  • Sink: Route handler callbacks at index.js:37 and subsequent route registrations, which directly read from config, dataDir, and fs without any prior credential check
  • Missing control: No authentication middleware (no req.headers token check, no session validation, no passport strategy) was present on any of the four affected route registrations
  • CWE: CWE-306 — Missing Authentication for Critical Function
  • Fix: Added apiAuth middleware that validates process.env.API_KEY against the x-api-key request header, applied to each sensitive route handler as a second argument to app.get()

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

Unauthenticated API endpoints are one of the most common and most preventable security issues in Node.js applications. The pattern is almost always the same: routes get added quickly during development, authentication is planned but deferred, and the application ships with open endpoints. In this case, four routes in index.js — handling config, subscriptions, sites, and refresh — were all reachable without a single credential check.

The fix is clean and instructive: a small apiAuth middleware function, a CSRF protection layer using crypto.randomBytes() for a secure secret, and path.basename() to neutralize path traversal in file reads. None of these changes are complex, but together they transform an open API into one that requires explicit authorization.

If you're building Express.js services, audit your route registrations today. A one-line grep can surface every unprotected endpoint in minutes — and Orbis AppSec can do it automatically on every pull request.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

Music Studio App Express Server: CWE-287 Authentication Bypass via

A critical authentication bypass vulnerability in a music studio application's Express server allowed remote attackers to access API endpoints despite the intended localhost-only restriction. The localRequest middleware validated the Host header but failed to verify the actual remote socket address, enabling trivial bypasses in containerized and multi-user environments.

high

CVE-2026-104850: MCP TypeScript SDK OAuth Credential Leak

The MCP TypeScript SDK (`@modelcontextprotocol/sdk`) contained a flaw in its OAuth client flow where the MCP server being connected to could influence which authorization server the client talked to, meaning client credentials and token-exchange traffic could be directed to an endpoint the attacker controls. This project was pinned to the affected `1.30.0`; the dependency has been moved to `^1.31.0`, which carries the upstream fix for CVE-2026-104850. Any MCP client that performs OAuth against t

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

high

undici 8.10.0 Cache Poisoning: CVE-2026-85152 Auth Bypass

undici, the HTTP client used by Node.js's `fetch()` implementation, shipped a caching layer that did not properly isolate cached responses by origin, letting a response poisoned on one origin be served to requests for another. This created a path to cross-origin authentication bypass, tracked as CVE-2026-85152 and fixed in undici 8.10.2.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.