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' })
});

Prevention & Best Practices

1. Apply Authentication at the Earliest Possible Point

The fix places the auth check before body parsing. This is the right pattern — don't do expensive or sensitive work before verifying the caller. In Express-style middleware:

// Middleware pattern — even better for multi-endpoint apps
function requireTranslateSecret(req, res, next) {
  const secret = process.env.TRANSLATE_API_SECRET;
  if (secret && req.headers['x-translate-secret'] !== secret) {
    return res.status(401).json({ error: 'Unauthorized.' });
  }
  next();
}

app.post('/api/translate', requireTranslateSecret, handler);

2. Never Expose Paid API Proxies Without Authentication

Any endpoint that proxies a paid third-party service (OpenAI, Anthropic, Google APIs, Twilio, SendGrid, etc.) is a direct financial liability if left unauthenticated. Treat these endpoints with the same care as payment endpoints.

3. Use Secrets Management, Not Hardcoded Values

The fix correctly reads the secret from process.env.TRANSLATE_API_SECRET. Never hardcode secrets in source. Use:
- Vercel: Project environment variables
- AWS: Secrets Manager or Parameter Store
- Docker: --env-file or mounted secrets

4. Supplement with Rate Limiting (Defense in Depth)

Authentication is the primary control. Rate limiting is a secondary layer. Even authenticated users could abuse the endpoint if no per-user limits exist:

import rateLimit from 'express-rate-limit';

const translateLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 20,             // 20 requests per minute per IP
  standardHeaders: true,
  legacyHeaders: false,
});

5. Monitor API Costs as a Security Signal

Set up billing alerts on Anthropic and Google Cloud. A sudden spike in API spend is often the first observable indicator of endpoint abuse. Treat cost anomalies as security incidents.

6. Relevant Standards

  • OWASP API Security Top 10 — API2:2023: Broken Authentication
  • CWE-306: Missing Authentication for Critical Function
  • OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

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.


References

Frequently Asked Questions

What is missing authentication on an API endpoint?

It means any caller — authenticated user or anonymous bot — can invoke the endpoint and trigger its backend logic without proving identity or authorization. In this case, that meant free access to paid AI translation services.

How do you prevent missing authentication in Node.js API handlers?

Add an authentication check at the very start of the handler function, before any business logic runs. Use environment-variable-stored secrets, JWT validation, or an API gateway to enforce identity on every request.

What CWE is missing authentication?

CWE-306 — "Missing Authentication for Critical Function." It describes situations where a function performing a sensitive or costly action is reachable without verifying the caller's identity.

Is rate limiting enough to prevent this type of abuse?

No. Rate limiting slows abuse but does not stop it. An attacker can still consume your paid API quota within the allowed rate, and in-memory rate limiters reset on cold starts. Authentication is the correct primary control.

Can static analysis detect missing authentication vulnerabilities?

Yes. Tools like Semgrep can be configured to flag exported handler functions that lack an authentication check before reaching a known sensitive sink (e.g., an API client call). Orbis AppSec's multi-agent AI scanner detected exactly this pattern in `api/translate.js`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #954

Related Articles

critical

How hardcoded API key exposure happens in JavaScript and how to fix it

A critical security vulnerability was discovered in `js/douban.js` where the Douban API key (`0ac44ae016490db2204ce0a042db2916`) was hardcoded directly in client-side JavaScript. This exposed the API credential to any user who could view the page source or use browser developer tools. The fix removed the hardcoded key, preventing unauthorized API access and potential abuse.

critical

How buffer overflow in Intel SGX enclave ECALLs happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in Intel SGX enclave functions `ecall_encrypt_data` and `ecall_decrypt_data` in `backend/sgx/enclave/enclave.c`. The functions performed memory operations without validating that the provided buffer lengths matched the actual allocated buffer sizes, allowing an attacker controlling the untrusted application to trigger heap corruption within the secure enclave by passing oversized length parameters.

critical

How Server-Side Request Forgery happens in Node.js server.js and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `server.js` and `worker.js`, where user-supplied `config` URL parameters were passed directly to `fetchWithAuth()` without any validation. This allowed attackers to force the application to make requests to internal network addresses, cloud metadata endpoints like `169.254.169.254`, or `file://` URIs. The fix introduces an `isAllowedUrl()` allowlist function that rejects private IP ranges, loopback addresses, and non-HTTP(S) pr

high

How Denial of Service via excessive memory allocation happens in Node.js adm-zip and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-39244) was discovered in adm-zip version 0.5.16, allowing attackers to crash Node.js applications by sending specially crafted ZIP files that trigger excessive memory allocation. The fix involves upgrading to adm-zip 0.6.0, which implements proper bounds checking during ZIP file decompression.

critical

How HTML sanitizer bypass via XMP tag passthrough happens in Node.js and how to fix it

A critical cross-site scripting (XSS) vulnerability in the sanitize-html library allowed attackers to bypass HTML sanitization through improper handling of the `<xmp>` raw-text element. This vulnerability could enable stored XSS attacks in any Node.js application using sanitize-html versions prior to 2.17.4. The fix involved upgrading the library to properly handle raw-text elements and prevent script injection.

high

How missing minimum release age happens in pnpm workspaces and how to fix it

A pnpm workspace configuration was missing the `minimumReleaseAge` setting, meaning freshly published — and potentially malicious or unstable — npm packages could be installed immediately. By adding `minimumReleaseAge: 10080` to `pnpm-workspace.yaml`, the project now enforces a seven-day waiting period before any newly released package version can be installed. This single configuration change significantly reduces the attack surface for supply chain attacks targeting this Node.js library and it