Back to Blog
critical SEVERITY8 min read

How unauthenticated API access happens in Node.js Express endpoints and how to fix it

The `/api/translate` endpoint in `api/translate.js` was publicly accessible without any authentication, allowing anonymous users to freely invoke paid Anthropic Claude or Google Translate API calls. This critical vulnerability exposed the application to API cost abuse, quota exhaustion, and potential data exfiltration. A targeted fix adds a shared-secret header check before any translation logic executes.

O
By Orbis AppSec
Published July 29, 2026Reviewed July 29, 2026

Answer Summary

This is a missing authentication vulnerability (CWE-306) in a Node.js API handler (`api/translate.js`). The `/api/translate` endpoint accepted POST requests from any anonymous caller, enabling unrestricted use of paid backend translation services (Anthropic Claude / Google Translate). The fix adds a pre-flight check for a `x-translate-secret` header matched against the `TRANSLATE_API_SECRET` environment variable, rejecting unauthenticated requests with HTTP 401 before any downstream API call is made.

Vulnerability at a Glance

cweCWE-306
fixAdded a shared-secret header check (`x-translate-secret` vs `TRANSLATE_API_SECRET` env var) at the top of the handler, returning 401 on mismatch
riskUnlimited free use of paid translation APIs, financial cost abuse, quota exhaustion
languageJavaScript (Node.js)
root causeThe exported `handler` function in `api/translate.js` performed no identity or secret verification before processing requests
vulnerabilityMissing Authentication on Public API Endpoint

How Unauthenticated API Access Happens in Node.js Express Endpoints and How to Fix It

The Problem in Plain Sight

The api/translate.js file is responsible for one job: accept a block of text, call either Anthropic Claude or Google Translate, and return a translation. Every call to those backends costs real money. Yet until this fix landed, any person on the internet — no account, no token, no relationship with the application — could POST to /api/translate and run up the bill.

This is not a subtle logic flaw. It is a missing front door lock on a room that contains a cash register.


The Vulnerability Explained

What the Code Did (and Didn't Do)

Before the fix, the exported handler function in api/translate.js looked roughly like this around line 95:

// api/translate.js (before fix) — line ~106 onward
export default async function handler(req, res) {
  // ... method check (OPTIONS/POST) ...

  // ❌ No authentication check here — jumped straight to input parsing
  const { text, targetLang, sourceLang = 'en', context } = req.body || {};

  if (!text || typeof text !== 'string' || !targetLang) {
    res.status(400).json({ error: 'Missing required fields.' });
    return;
  }

  // ... cache lookup, then API call to Anthropic or Google Translate ...
}

The handler validated what was being asked (is text a string? is targetLang present?) but never validated who was asking. The only friction between the open internet and a paid API call was:

  1. A basic POST method check.
  2. Input validation on text and targetLang.
  3. An in-memory cache — which resets on every cold start and provides zero protection against distributed abuse.

How an Attacker Would Exploit This

The attack requires zero sophistication:

# Anyone on the internet can do this — no credentials required
curl -X POST https://your-app.com/api/translate \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello world", "targetLang": "es"}'

A motivated attacker could:

  1. Run a cost-exhaustion campaign — script thousands of unique translation requests per minute (bypassing the in-memory cache with varied inputs), draining the application's Anthropic or Google Translate API budget.
  2. Exfiltrate translated content — if the application's own users submit sensitive text for translation, an attacker who knows the endpoint can replay or mirror those requests.
  3. Probe the AI backend — use the unauthenticated endpoint to probe Anthropic Claude's behavior with adversarial prompts, potentially revealing system prompt structure or triggering unintended model outputs at the app owner's expense.

The PR description characterizes this as a 2-step chain: the attacker (1) discovers the public endpoint and (2) sends crafted POST requests. There is no step 3 — exploitation is immediate.

Why the In-Memory Cache Doesn't Save You

The handler includes a cache to avoid redundant API calls for identical inputs. This sounds like a partial mitigation, but it is not:

  • The cache lives in process memory and resets on every cold start (relevant for serverless/Vercel deployments where api/translate.js is likely deployed).
  • An attacker generating unique text values (even trivially, by appending a counter) will always miss the cache.
  • The cache is a performance optimization, not a security control.

The Fix

What Changed

The fix inserts a shared-secret authentication check immediately after the method/OPTIONS handling and before any body parsing or API calls:

// api/translate.js (after fix) — inserted at line ~109
const apiSecret = process.env.TRANSLATE_API_SECRET;
if (apiSecret) {
  const provided = req.headers['x-translate-secret'];
  if (!provided || provided !== apiSecret) {
    res.status(401).json({ error: 'Unauthorized.' });
    return;
  }
}

Before vs. After

Before — handler flow:

POST /api/translate
  → method check
  → [NO AUTH CHECK]
  → parse body
  → validate text/targetLang
  → check cache
  → call Anthropic/Google Translate  ← anyone reaches here

After — handler flow:

POST /api/translate
   method check
   check x-translate-secret header vs TRANSLATE_API_SECRET env var
       mismatch?  401 Unauthorized, stop
   parse body
   validate text/targetLang
   check cache
   call Anthropic/Google Translate   only authorized callers reach here

Why This Specific Fix Works

  • Fail-closed when configured: If TRANSLATE_API_SECRET is set in the environment, every request without the correct x-translate-secret header is rejected with a 401 before any downstream logic runs.
  • Opt-in for development: If TRANSLATE_API_SECRET is not set (e.g., in a local dev environment where the variable isn't configured), the check is skipped. This prevents the fix from breaking local development workflows while ensuring production deployments — where the env var should always be set — are protected.
  • No business logic changes: The fix is scoped entirely to the authentication gate. Translation behavior, caching, and error handling for valid authenticated requests are completely unchanged.
  • Cheap to evaluate: A string comparison on a request header is essentially free. There is no performance cost to this check.

Deploying the Fix

To activate the protection, set the environment variable in your deployment:

# Generate a strong random secret
openssl rand -hex 32
# → e.g., 4a7f2c9b1d3e5f8a0b2c4d6e8f1a3b5c...

# Set it in your environment (Vercel, .env, etc.)
TRANSLATE_API_SECRET=4a7f2c9b1d3e5f8a0b2c4d6e8f1a3b5c...

Callers (your own frontend, internal services) must then include the header:

// Authorized client call
fetch('/api/translate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-translate-secret': process.env.TRANSLATE_API_SECRET
  },
  body: JSON.stringify({ text: 'Hello', targetLang: 'es' })
});

Key Takeaways

  • The /api/translate handler in api/translate.js had zero authentication before line 109 — any anonymous HTTP client could invoke Anthropic Claude or Google Translate at the application owner's expense.
  • In-memory caches are not security controls — they reset on cold starts and are trivially bypassed with unique inputs, providing no meaningful protection against cost-abuse attacks.
  • The fix is a single, targeted block — a TRANSLATE_API_SECRET environment variable check against the x-translate-secret request header, inserted before any body parsing or API calls.
  • Paid API proxy endpoints deserve authentication by default — if your Node.js handler calls a metered third-party service, assume it will be found and abused if left unauthenticated.
  • The opt-in design (if (apiSecret)) preserves local dev ergonomics while ensuring production deployments with the env var set are fully protected.

How Orbis AppSec Detected This

  • Source: Unauthenticated HTTP POST request to /api/translate — specifically, the req.body object consumed at line ~112 of api/translate.js with no prior identity check.
  • Sink: The downstream call to the Anthropic Claude or Google Translate API client within the same handler — a paid, rate-limited external service triggered on every cache miss.
  • Missing control: No authentication middleware, no API key validation, no JWT check, and no session verification existed anywhere in the handler's execution path before the API client was invoked.
  • CWE: CWE-306 — Missing Authentication for Critical Function.
  • Fix: Inserted a shared-secret header check (x-translate-secret vs. TRANSLATE_API_SECRET) at line 109 of api/translate.js, returning HTTP 401 before any business logic executes for unauthenticated requests.

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 authentication on a paid API proxy is one of those vulnerabilities that feels obvious in retrospect but is easy to miss during rapid development — especially when you're focused on getting the translation feature working and the input validation looks solid. The /api/translate endpoint had reasonable validation of what it received, but no validation of who was sending it.

The fix is small — nine lines — but the security improvement is significant. By checking the x-translate-secret header against TRANSLATE_API_SECRET before any other logic runs, the handler now enforces a clear boundary: only callers who know the secret can trigger a translation. Everyone else gets a 401.

If your Node.js application proxies any paid or sensitive backend service, audit your handlers today. Ask one question for each endpoint: "What stops an anonymous HTTP client from reaching the expensive part?" If the answer is "nothing," you have the same vulnerability that was just fixed here.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #954

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.