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
Published August 26, 2026Reviewed 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.


Prevention & Best Practices

1. Apply Auth Middleware at the Router Level

Instead of adding apiAuth to every individual route, mount it on the router prefix:

const apiRouter = express.Router();
apiRouter.use(apiAuth); // applies to ALL routes on this router

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

app.use('/api', apiRouter);

This prevents accidentally forgetting to add apiAuth to a new route.

2. Never Rely on CORS Alone

CORS headers are browser-enforced only. curl, Python's requests, Postman, and any server-side HTTP client will completely ignore Access-Control-Allow-Origin. Always pair CORS with real authentication.

3. Store Secrets in Environment Variables

The fix correctly reads process.env.API_KEY rather than hardcoding a value. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, or even a .env file excluded from version control) for production deployments.

4. Audit All Route Registrations

Run a quick grep on your codebase to find unprotected routes:

grep -n "app\.get\|app\.post\|app\.put\|app\.delete" index.js | grep -v "apiAuth\|authMiddleware\|authenticate"

Any line that doesn't reference an auth middleware is a candidate for review.

5. OWASP & Standards References

This vulnerability falls under:
- OWASP API Security Top 10 — API2:2023: Broken Authentication
- OWASP Top 10 — A07:2021: Identification and Authentication Failures
- CWE-306: Missing Authentication for Critical Function


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.


References

Frequently Asked Questions

What is missing authentication in Express.js APIs?

Missing authentication means HTTP route handlers process requests without verifying the caller's identity — no token, session, or credential check is performed before returning data or executing logic.

How do you prevent missing authentication in Node.js?

Apply authentication middleware (e.g., checking an `x-api-key` header against an environment variable, or validating a JWT) to every sensitive route, ideally by mounting it on the route prefix with `app.use('/api', authMiddleware)`.

What CWE is missing authentication?

CWE-306 — "Missing Authentication for Critical Function" — covers cases where software does not authenticate users before granting access to sensitive functionality.

Is CORS configuration enough to prevent unauthorized API access?

No. CORS is enforced by browsers but can be trivially bypassed by server-side tools like `curl` or custom HTTP clients. Proper authentication middleware is required.

Can static analysis detect missing authentication?

Yes. Tools like Semgrep, Snyk, and multi-agent AI scanners (as used here) can identify Express.js route handlers that lack authentication middleware by analyzing the route registration call signatures.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

How User Enumeration Happens in Django Forms and How to Fix It

A critical user enumeration vulnerability in the volunteers application allowed attackers to systematically discover registered email addresses through distinct error messages in signup and password reset forms. The fix replaces specific error messages with generic ones, preventing information disclosure while maintaining application functionality.

critical

How Authentication Bypass Happens in Node.js WebSocket Services and How to Fix It

The HousePanel push notification service exposed GET and POST endpoints without any authentication checks, allowing unauthenticated attackers to send arbitrary push notifications to connected smart devices. This critical vulnerability was fixed by implementing mandatory token validation on all protected endpoints, ensuring only authenticated requests can trigger push operations.

critical

How Missing API Authentication Happens in Node.js and How to Fix It

The GitHub API integration in `src/github.mjs` was making unauthenticated requests, subjecting the application to GitHub's strict 60 requests/hour rate limit. This fix adds secure authentication token injection from environment variables using conditional header spreading, enabling authenticated requests with a much higher rate limit (5,000 requests/hour).

high

How OAuth 2.0 Authorization Code Interception happens in PHP and how to fix it

The Weibo OAuth login implementation in `trunk/web/login_weibo.php` was missing PKCE (Proof Key for Code Exchange), allowing attackers with network access to exchange intercepted authorization codes for access tokens. The fix adds cryptographic binding between the authorization request and token exchange using SHA256 code challenges.

high

How OAuth Token Binding Prevents Session Hijacking in Weibo Login Implementation

A critical vulnerability in the Weibo OAuth login implementation allowed attackers to replay stolen access tokens across different user sessions. By binding the OAuth access token to the session ID using cryptographic hashing, the fix ensures that intercepted tokens cannot be reused to hijack other sessions, even if compromised via MITM or XSS attacks.

high

How Unauthenticated Endpoint Exposure Happens in Node.js and How to Fix It

A high-severity unauthenticated endpoint exposure was discovered in `dep/src/server/index.js`, where the `/--ziko--` route served internal application state (`globalThis.Ziko`) to any network-connected client without any authentication or environment guard. The fix adds a single production environment check that returns a `404` before the sensitive data is ever sent. This kind of "debug route left in production" vulnerability is surprisingly common in Node.js applications and can silently leak c