A critical gap in a cost-bearing endpoint
Imagine deploying a resume generation service backed by OpenAI's API—a feature that incurs real costs per request. Now imagine that any caller on your network can invoke it without proving who they are. That's the reality of this vulnerability.
The POST /api/generate endpoint in the resume service had no authentication middleware. It simply accepted requests and passed them to the resume controller, which then made expensive OpenAI API calls. There was no check of identity, no credential validation, no rate limiting by user. The endpoint was, in effect, public to anyone with network access.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) |
| Ecosystem | Node.js / npm |
| CVE / GHSA | not assigned |
| CWE | CWE-862 (Missing Authorization) |
The Vulnerability Explained
Before the fix, the route definition looked like this:
const express = require('express');
const router = express.Router();
const resumeController = require('../controllers/resumeController');
// POST /api/generate
router.post('/generate', resumeController.generateResume);
Anyone could make a POST request to /api/generate and trigger resume generation. There was no middleware intercepting the request to verify the caller's identity. The endpoint went straight from the HTTP listener to the business logic.
Why is this dangerous?
Resume generation is not free. Each call to generateResume likely invokes OpenAI's API, consuming tokens and incurring charges. An attacker who discovers this endpoint can:
- Make thousands of requests in rapid succession to exhaust the API quota
- Force the service to incur unexpected bills
- Trigger a denial of service by consuming all available API calls for legitimate users
- Do all of this from inside the local network, across container boundaries, or via a compromised machine on the same subnet
The attack in practice:
An attacker on the same network (or with access to an internal service) writes a simple loop:
for i in {1..10000}; do
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d '{"input": "My resume data"}'
done
Within minutes, thousands of resume generations execute, each hitting the OpenAI API. The service owner receives a bill for thousands of API calls they never authorized. Meanwhile, legitimate users attempting to generate resumes find the quota exhausted.
The vulnerability exists because the route has no authorization control—no way to verify that the caller is allowed to use this resource.
The Fix
The fix introduces a middleware function called requireApiKey() that runs before the route handler:
function requireApiKey(req, res, next) {
const expectedKey = process.env.API_KEY;
if (!expectedKey) {
console.error('API_KEY is not configured on the server');
return res.status(500).json({ success: false, message: 'Server misconfiguration' });
}
const providedKey = req.header('x-api-key');
if (providedKey !== expectedKey) {
return res.status(401).json({ success: false, message: 'Unauthorized' });
}
return next();
}
router.use(requireApiKey);
router.post('/generate', resumeController.generateResume);
What changed:
-
Environment variable setup: The
.env.examplefile now includesAPI_KEY=your_app_api_key_here, signaling to operators that they must generate and configure a secret key before deployment. -
Middleware validation: The
requireApiKey()function checks that:
-API_KEYis configured (if not, returns 500 to alert operators)
- Thex-api-keyheader in the incoming request matches the server'sAPI_KEY
- If the keys don't match, the request is rejected with a 401 Unauthorized response -
Route protection:
router.use(requireApiKey)applies this middleware to all routes defined on the router, including POST /api/generate. The middleware runs before the route handler, so no request reachesresumeController.generateResume()unless the API key is valid.
Why this solves the problem:
- Before: Any request could reach the controller.
- After: Only requests carrying the correct API key in the
x-api-keyheader proceed.
An attacker trying the previous attack now receives:
HTTP/1.1 401 Unauthorized
{"success": false, "message": "Unauthorized"}
The request never reaches the OpenAI API. The attacker has no way to generate resumes unless they obtain the secret API_KEY value.
How the attack changes with the fix
With the fix in place, an attacker's loop now fails immediately:
curl -X POST http://localhost:5000/api/generate
# HTTP/1.1 401 Unauthorized
# {"success": false, "message": "Unauthorized"}
To exploit the endpoint, the attacker must now:
1. Discover or guess the API_KEY value (a random secret)
2. Pass it in every request via the x-api-key header
This requirement transforms the attack from trivial to cryptographically hard. The service is no longer exposed to unauthenticated quota theft.
Key Takeaways
-
Cost-bearing endpoints need authentication, not just CORS headers. CORS (Cross-Origin Resource Sharing) controls which origins can call your API; it says nothing about who can call it. OpenAI API endpoints require explicit credential checks.
-
Environment variables for secrets belong in
.env.example. By addingAPI_KEY=your_app_api_key_hereto the example config, operators are reminded to generate and configure a unique key before deployment. Omitting this from the example invites forgotten configuration and accidental exposure. -
Middleware applies in declaration order. By placing
router.use(requireApiKey)before the route definition, every subsequent route on that router is protected. This is simpler and less error-prone than protecting individual routes. -
Request headers are the standard transport for API keys in REST services. The fix uses the
x-api-keyheader, which is widely recognized and doesn't conflict with HTTP authentication headers. This makes the API predictable for clients. -
Authorization failures should return 401, not 403. A 401 (Unauthorized) tells the client "you need to authenticate"; a 403 (Forbidden) says "you are authenticated but not allowed." This distinction helps clients distinguish between missing credentials and insufficient permissions.
How Orbis AppSec Detected This
Source: The HTTP request body and headers to the POST /api/generate endpoint accept any incoming request without authentication.
Sink: The resumeController.generateResume() function invokes the OpenAI API based on unauthenticated input, incurring costs and consuming quota.
Missing control: No authentication or authorization middleware validates the caller's identity before the resume generation function executes.
CWE: CWE-862 (Missing Authorization)
Fix: Add a requireApiKey() middleware that validates the x-api-key HTTP header against a server-side API_KEY environment variable before allowing any resume route to execute.
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
Missing authorization on cost-bearing endpoints is a practical, high-impact vulnerability. An attacker with network access doesn't need to be sophisticated—they just need to know the endpoint exists. This fix demonstrates that protecting such endpoints requires explicit, layered validation: the endpoint should demand proof of identity before executing expensive operations. The API key middleware pattern used here is standard in REST services and scales from single endpoints to entire route groups. Deploy it consistently across any endpoint that triggers real-world costs or resource consumption.