Skip to content

fix: use dedicated header with timing-safe comparison for rate-limit bypass token (CWE-863) - #246

Open
andesyteoss wants to merge 1 commit into
exa-labs:mainfrom
andesyteoss:security/cwe863-bypass-token
Open

fix: use dedicated header with timing-safe comparison for rate-limit bypass token (CWE-863)#246
andesyteoss wants to merge 1 commit into
exa-labs:mainfrom
andesyteoss:security/cwe863-bypass-token

Conversation

@andesyteoss

@andesyteoss andesyteoss commented Mar 26, 2026

Copy link
Copy Markdown

Vulnerability Summary

CWE: CWE-863 — Incorrect Authorization
Severity: High
File: api/mcp.ts
Affected code path: handleRequest → bypass-token check (line ~336)

Data Flow

  1. An HTTP request arrives at the public Vercel endpoint (https://mcp.exa.ai/mcp), exported as GET, POST, and DELETE.
  2. The User-Agent header (attacker-controlled) is read and compared against the RATE_LIMIT_BYPASS environment variable using userAgent.startsWith(bypassPrefix).
  3. If the prefix matches and EXA_API_KEY_BYPASS is configured, the request is granted access to the dedicated bypass API key — effectively free, billed-to-operator API usage.

Three Distinct Flaws

# Issue Impact
1 startsWith prefix match — any string beginning with the secret token passes the check Weakens the token to its shortest unique prefix; partial knowledge of the token is sufficient
2 Secret in User-Agent headerUser-Agent is logged by virtually all infrastructure (CDN, Vercel function logs, WAF, access logs) The bypass token is exposed to anyone with log access (information disclosure)
3 No constant-time comparison — string comparison leaks token length/content via timing Enables timing side-channel (low risk over network, but defense-in-depth best practice)

Exploit Sketch

# If RATE_LIMIT_BYPASS = "secret-token-abc123"

# Exact match works (expected):
curl -X POST https://mcp.exa.ai/mcp \
  -H 'User-Agent: secret-token-abc123' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"web_search_exa","arguments":{"query":"test"}},"id":1}'

# Prefix-only ALSO works (this is the bug):
curl -X POST https://mcp.exa.ai/mcp \
  -H 'User-Agent: secret-token-abc123-anything-extra' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"web_search_exa","arguments":{"query":"test"}},"id":1}'

Preconditions

  1. RATE_LIMIT_BYPASS and EXA_API_KEY_BYPASS environment variables must be configured (they are on the production deployment at mcp.exa.ai).
  2. Attacker needs to know the RATE_LIMIT_BYPASS value — but since it is transmitted in User-Agent, it is logged by virtually all web infrastructure, making it discoverable by anyone with log access.

Fix Description

Single file changed: api/mcp.ts (+25 / −5 lines)

Change Rationale
Move token from User-Agent to dedicated X-Bypass-Token header X-Bypass-Token is not logged by default in CDN/server access logs, preventing secret leakage
Replace startsWith with exact-match comparison Eliminates prefix-match bypass; token must match exactly
Use timingSafeEqual from node:crypto Prevents timing side-channel attacks (defense-in-depth)
Add RATE_LIMIT_BYPASS_TOKEN env var with fallback to RATE_LIMIT_BYPASS Backward-compatible migration path for operators

What the fix does NOT change

  • No changes to rate-limiting logic, MCP tool definitions, or any other functionality.
  • The bypass capability still works for legitimate users — they just need to send the token in X-Bypass-Token instead of User-Agent.

Test Results

Verification of the fix logic

  • Exact match via X-Bypass-Token — bypass is granted (correct behavior)
  • Prefix-only match — bypass is denied (fixes the vulnerability)
  • Empty header — bypass is denied
  • Missing env vars — bypass is denied (no crash)
  • User-Agent with old token — bypass is no longer granted (migration to new header)
  • RATE_LIMIT_BYPASS_TOKEN takes precedence over legacy RATE_LIMIT_BYPASS when both are set
  • timingSafeEqual length check — mismatched lengths return false without timing leak

Disprove Analysis

We systematically attempted to disprove this finding:

Check Result
Auth guard No other authentication guards the bypass path. The startsWith check IS the auth mechanism — and it is the one being fixed.
Network restriction None. This is a Vercel serverless function at a public URL. No CORS restrictions, no localhost binding, no VPN.
Deployment context Vercel serverless (vercel.json). Public internet. No reverse proxy filtering.
Caller trace handleRequest is directly exported as GET, POST, DELETE. Any HTTP request reaches the vulnerable code.
Input validation No sanitization or validation on User-Agent or any header before the bypass check.
Prior reports No security-related issues found in the GitHub repo.
Security policy No SECURITY.md exists.
Similar patterns No other startsWith-based auth checks remain in the codebase after this fix.
Parallel paths There is exactly ONE code path that uses EXA_API_KEY_BYPASS — this is it.

Mitigations Found

  • IP-based rate limiting still applies to bypass users (limits blast radius, but attacker still gets free API usage).
  • Both RATE_LIMIT_BYPASS and EXA_API_KEY_BYPASS must be configured (if the operator does not set these, there is no vulnerability).

Verdict

CONFIRMED_VALID — High confidence. The vulnerability is real and exploitable on the internet-facing production deployment. The fix addresses all three identified flaws and is minimal/targeted.


…bypass (CWE-863)

Replace User-Agent prefix matching (startsWith) with a dedicated
X-Bypass-Token header verified via crypto.timingSafeEqual.

The old mechanism checked if the User-Agent started with a configured
prefix (RATE_LIMIT_BYPASS env var). This is insecure because:

1. User-Agent is a publicly visible, client-controlled header that
   offers no secrecy guarantees.
2. startsWith allows any suffix, so partial knowledge of the prefix
   (or a short/guessable value) is enough to bypass rate limits.
3. No timing-safe comparison was used, enabling timing side-channels.

The fix:
- Reads the token from X-Bypass-Token header (not User-Agent)
- Requires exact match via timingSafeEqual (constant-time)
- Supports RATE_LIMIT_BYPASS_TOKEN env var (falls back to RATE_LIMIT_BYPASS
  for backwards compatibility during migration)
@vercel

vercel Bot commented Mar 26, 2026

Copy link
Copy Markdown

@sebastiondev is attempting to deploy a commit to the Exa Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant