Skip to content

fix(register): allowlist headers persisted at client registration#264

Open
sebastiondev wants to merge 1 commit into
neondatabase:mainfrom
sebastiondev:fix/cwe200-route-all-e783
Open

fix(register): allowlist headers persisted at client registration#264
sebastiondev wants to merge 1 commit into
neondatabase:mainfrom
sebastiondev:fix/cwe200-route-all-e783

Conversation

@sebastiondev
Copy link
Copy Markdown

Vulnerability summary

The POST /api/register endpoint in landing/app/api/register/route.ts iterates over all incoming request headers and persists them into the KV store via model.saveClientRegisterHeaders(). This endpoint is unauthenticated — it implements the OAuth 2.0 Dynamic Client Registration flow — and is reachable on the public internet at mcp.neon.tech.

Because every header is stored, sensitive values end up persisted:

  • Authorization — bearer tokens from upstream proxies or mistaken client configurations
  • Cookie — session cookies attached by browsers or intermediaries
  • X-Forwarded-For / X-Forwarded-Proto / X-Real-IP — internal infrastructure topology
  • Any other header injected by load balancers, CDNs, or clients

These stored headers are later read back in the /authorize flow, meaning the leaked data could also be reflected to other parties.

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Affected file: landing/app/api/register/route.ts, lines 14–17 (the request.headers.forEach loop)

Data flow: request.headersrequestHeaders object (all keys) → model.saveClientRegisterHeaders(clientId, requestHeaders) → Postgres-backed KV store → read back in /authorize

Proof of Concept

curl -X POST https://mcp.neon.tech/api/register \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer leaked-secret-token" \
  -H "Cookie: session=sensitive-value" \
  -H "X-Forwarded-For: 10.0.0.42" \
  -H "X-Internal-Debug: should-not-persist" \
  -H "X-Read-Only: true" \
  -d '{
    "client_name": "test-client",
    "redirect_uris": ["https://example.com/callback"],
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }'

All five custom headers above would be persisted into the KV store. Only x-read-only is actually consumed by the authorize handler — the rest are sensitive data leaking into persistent storage.

Fix description

The fix introduces a strict allowlist of headers that may be persisted:

const ALLOWED_REGISTER_HEADERS = [
  'x-read-only',
  'x-neon-read-only',
  'x-neon-project-id',
  'x-neon-scopes',
];

The forEach loop now checks ALLOWED_REGISTER_HEADERS.includes(lower) before adding a header to the requestHeaders object. This ensures only the four headers that the authorize flow actually consumes are stored — everything else is silently dropped.

This is a minimal, behavior-preserving change: the authorize handler only reads x-read-only (and potentially the other x-neon-* headers), so no legitimate functionality is affected.

Test results

A new integration test (does not persist sensitive or unknown headers) was added to landing/mcp-src/__tests__/oauth-register-route.integration.test.ts. The test sends a registration request with both allowed (x-read-only: true) and sensitive (authorization, cookie, x-forwarded-for, x-internal-trace) headers, then asserts:

  • Only { 'x-read-only': 'true' } is passed to saveClientRegisterHeaders
  • None of the sensitive headers appear in the persisted object
  • The registration still succeeds (200 response with a valid client_id)

All existing tests continue to pass — the fix does not change behavior for legitimate registrations.

Security analysis

This is exploitable by any unauthenticated network client. The /api/register endpoint requires no authentication (by design — it implements RFC 7591 Dynamic Client Registration). An attacker can craft requests with arbitrary headers, and all of them are written to persistent storage. The risk compounds because:

  1. Infrastructure headers (added by Vercel/CDN) reveal internal topology
  2. Auth headers from legitimate users hitting the same endpoint could be captured if a proxy or browser attaches them
  3. The stored headers are read back in the authorize flow, creating a secondary exposure surface

Before submitting, we verified that no existing middleware or framework protection filters headers before they reach the route handler — Next.js App Router passes the full NextRequest headers object through unchanged, and there is no header-stripping middleware in the Vercel deployment configuration.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

Previously the dynamic client registration handler stored every request
header on the new client record. The handler iterated over
request.headers and persisted them verbatim via
model.saveClientRegisterHeaders, which meant that headers like
Authorization, Cookie, and infrastructure-injected headers
(X-Forwarded-For, internal trace IDs, etc.) were saved into the KV
store and later re-read in the authorize flow.

The authorize handler only consults a small set of x-read-only /
x-neon-* headers, so restrict the persisted headers to that allowlist.
Adds a regression test that asserts sensitive headers are not stored.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

@sebastiondev is attempting to deploy a commit to the neondatabase 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