Skip to content

fix: redact and strip exaApiKey from request URLs to prevent log exposure (CWE-200) - #239

Closed
andesyteoss wants to merge 1 commit into
exa-labs:mainfrom
andesyteoss:security/cwe200-redact-api-key-from-logs
Closed

fix: redact and strip exaApiKey from request URLs to prevent log exposure (CWE-200)#239
andesyteoss wants to merge 1 commit into
exa-labs:mainfrom
andesyteoss:security/cwe200-redact-api-key-from-logs

Conversation

@andesyteoss

Copy link
Copy Markdown

Vulnerability Summary

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Severity: Medium
Affected file: api/mcp.ts

Data Flow

When a user authenticates via the documented ?exaApiKey= query parameter pattern (e.g. https://mcp.exa.ai/mcp?exaApiKey=exa-abc123xyz), the API key flows through several exposure paths:

  1. Debug logging (line 330, pre-fix): console.log(\[EXA-MCP] Request URL: ${request.url}`)— directly logs the full URL including the API key whendebug=true`.

  2. Downstream handler propagation: The request object (with full URL including exaApiKey) is passed to handler(request) from mcp-handler. Inside that library, the URL is:

    • Serialized into createFakeIncomingMessage({url: req.url}) (mcp-handler line 330)
    • Published to Redis including url: req.url (mcp-handler line 592)
    • Logged via logger.log("Published requests:...", serializedRequest) (mcp-handler line 640)
  3. Vercel platform logs: Vercel automatically captures request URLs in Function Logs in the dashboard. Anyone with project access sees full API keys.

  4. HTTP Referer headers: If any downstream request is made from the handler context, the full URL may be sent as the Referer header to third parties.

Related Issues


Fix Description

This PR adds two utility functions and applies them in handleRequest():

1. redactUrl(urlString) — for debug logs

Replaces the exaApiKey parameter value with REDACTED so the debug log line becomes safe:

// Before (vulnerable):
console.log(`[EXA-MCP] Request URL: ${request.url}`);
// After (fixed):
console.log(`[EXA-MCP] Request URL: ${redactUrl(request.url)}`);

2. stripApiKeyFromUrl(urlString) — for downstream propagation

Completely removes the exaApiKey parameter from the URL. Applied after the key has already been extracted into config, so it does not affect authentication:

request = new Request(stripApiKeyFromUrl(request.url), request);

Rationale

  • The API key is already extracted into config via getConfigFromRequest() before the strip runs, so no behavioral change occurs.
  • Both functions include a try/catch with regex fallback in case URL parsing fails.
  • The fix is defense-in-depth: even if downstream libraries or platform logging capture the request URL, the API key will no longer be present.

Test Results

21/21 tests passing

The test file test-cwe200-api-key-redaction.mjs covers:

Category Tests Status
stripApiKeyFromUrl basic functionality 5
stripApiKeyFromUrl edge cases (empty value, duplicates, long keys) 3
redactUrl basic functionality 5
redactUrl edge cases 3
Integration (full handleRequest flow simulation) 5
=== stripApiKeyFromUrl Tests ===
  ✅ Strips exaApiKey completely from URL
  ✅ Strips exaApiKey while preserving other params
  ✅ Strips exaApiKey from middle of params
  ✅ URL without exaApiKey is unchanged
  ✅ URL with no query params is unchanged
  ✅ Pathname preserved when exaApiKey is only param
  ✅ Key value removed
  ✅ Empty exaApiKey value is stripped
  ✅ Multiple exaApiKey params are all stripped
  ✅ Very long API key is stripped

=== Integration: handleRequest flow ===
  ✅ API key extracted from URL correctly
  ✅ Debug log shows REDACTED not the real key
  ✅ Stripped URL has no trace of API key
  ✅ Other params preserved in stripped URL
  ✅ Final URL passed to handler has no API key
  ✅ Pathname rewrite still works
  ✅ URL without exaApiKey is unchanged by stripping

=== Vulnerability confirmation (before fix) ===
  ✅ [Vulnerable] Old debug log exposes full API key
  ✅ [Fixed] New debug log redacts API key
  ✅ [Vulnerable] Old code passes API key to downstream handler in URL
  ✅ [Fixed] New code strips API key before passing to handler

Total: 21  Passed: 21  Failed: 0

Disprove Analysis

We attempted to invalidate this finding through multiple angles:

Authentication check

The API key is passed via Authorization: Bearer header or ?exaApiKey= query parameter. There is no separate auth guarding the endpoint — the API key IS the authentication. The fix protects the key from being leaked in logs.

Network check

No localhost-only restriction. This is deployed publicly at mcp.exa.ai on Vercel. The mcp-handler library sets Access-Control-Allow-Origin: "*". This is internet-facing.

Mitigations found

  1. Debug logging is off by defaultdebug defaults to false unless ?debug=true or DEBUG=true env var. This limits console.log exposure to opt-in scenarios.
  2. Authorization header is supported — Users who use the Authorization: Bearer header instead of query params are not affected.
  3. Platform-level URL logging is inherent to the query-string pattern — this is a well-known anti-pattern (OWASP: "Sensitive Data in GET Request Parameters").

Preconditions for exploitation

  • Vercel platform logging: No precondition beyond normal usage — Vercel logs all request URLs by default. Anyone with Vercel project access can see API keys.
  • Debug log exposure: Requires debug=true (opt-in).
  • Redis serialization: Requires SSE transport with Redis configured.

Prior art

  • CWE-200 is a well-established vulnerability class.
  • OWASP explicitly flags API keys in URL query strings.
  • Vercel documentation confirms request URLs are visible in Function Logs.

Verdict: CONFIRMED_VALID (high confidence)

The vulnerability is real. The fix is minimal, correct, and does not change application behavior since the key is extracted into config before being stripped from the URL.


Change Summary

 api/mcp.ts                        |  39 +++++-
 test-cwe200-api-key-redaction.mjs | 257 ++++++++++++++++++++++++++++++
 2 files changed, 294 insertions(+), 2 deletions(-)

Thank you for your consideration. Happy to adjust anything based on feedback.

…sure (CWE-200)

- Add redactUrl(): replaces exaApiKey values with "REDACTED" for debug logs
- Add stripApiKeyFromUrl(): removes exaApiKey param entirely from the URL
  after extraction, so downstream libraries and platform request logs
  never see the secret
- Use redactUrl() in debug console.log instead of raw request.url
- Strip exaApiKey from request URL before passing to mcp-handler
@vercel

vercel Bot commented Mar 25, 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.

@lewiswigmore

Copy link
Copy Markdown

Closing this to reduce the open-PR pile-up — we have multiple outstanding security contributions to this repo and that volume is not fair on your review queue. Keeping #246 (fix: use dedicated header with timing-safe comparison for rate-limit bypass toke) as the primary one to focus attention on.

Happy to revisit this finding separately later if it is still relevant. Apologies for the noise.

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.

2 participants