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/refreshinindex.jswere all publicly accessible — a single omission of middleware exposed the entire API surface- The
apiAuthmiddleware pattern (checkprocess.env.API_KEYagainstreq.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 automaticallypath.basename()is a one-line fix for path traversal in file-reading routes — always sanitize filenames derived from request data before passing them tofsfunctions- Graceful degradation (
if (!apiKey) return next()) makes the auth middleware developer-friendly without sacrificing production security — just setAPI_KEYin 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:37and subsequent route registrations, which directly read fromconfig,dataDir, andfswithout any prior credential check - Missing control: No authentication middleware (no
req.headerstoken check, no session validation, nopassportstrategy) was present on any of the four affected route registrations - CWE: CWE-306 — Missing Authentication for Critical Function
- Fix: Added
apiAuthmiddleware that validatesprocess.env.API_KEYagainst thex-api-keyrequest header, applied to each sensitive route handler as a second argument toapp.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.