nav logo
Product
Platform Overview
AI security and control platform.
Docs
Technical guides and references.
Resources
Customer Stories
Real-world Traceforce success stories.
Blogs
Insights on AI security.
Security Advisories
Vulnerability research and CVEs.
Pricing
Simple pricing that scales.
Company
About Us
The team behind Traceforce.
Legal
Policies, terms, and compliance.
Contact Us
Talk to our team.
Get started
Get started
Get Started
Contact Us
Get started
Security Advisory

TFSA-2026-006 : OAuth authorization-code interception in the Neon MCP broker

Severity
High
CVSS 3.1
Not scored
Published
September 24, 2026
Arrow
Back to advisories
Table of Contents
Advisory details
Product:
Neon MCP server (mcp.neon.tech)
Affected versions:
Neon MCP OAuth broker (mcp.neon.tech), tested July 2, 2026
Patched version:
CVSS 3.1 vector:
CWE:
CWE-352 Cross-Site Request Forgery; CWE-287 Improper Authentication
Publication status:
Traceforce advisory; no CVE assigned

Overview

A test user clicks Authorize on a legitimate Neon consent screen for the first-party MCP server. In the implementation we tested, the downstream client ID, redirect URI and scopes were carried in an unsigned, base64-encoded OAuth state value.

Legitimate Neon consent screen
Figure 1. The test user sees a legitimate Neon consent screen for the first-party MCP server. The downstream client and redirect URI are never shown. Account identifier redacted.

By modifying that routing information, we caused the authorization code to be forwarded to a redirect URI associated with a client we controlled. We then redeemed the code and confirmed access under the approving user's authorization context, including access to database credentials returned through the tested API.

Redirect URI carrying the code
Figure 2. After approval the browser lands on the testing client's redirect URI with the code in the address bar. No listener is needed. The code is redacted.

We reproduced the behavior on July 2, 2026 using only a controlled test account. We did not access any third-party account, and we have not subsequently re-verified the current implementation.

The full authorization-code interception chain
Diagram: the full chain. Step 7 is where it breaks.

What was actually in state

Decoding the base64 yields the downstream routing data, written by the testing client:

{
  "clientId": "<test-registered client id>",
  "redirectUri": "http://127.0.0.1:9300/grab",
  "scope": ["read", "write"],
  "state": "<downstream state>"
}
Modified state value
Figure 3. The state value is built as base64-encoded JSON carrying the downstream routing data. The client ID is redacted.

The base64 is only encoding, not protection. In the tested implementation, the routing data could be modified and lacked sufficient integrity protection and browser-session binding. When the code comes back, the broker reads redirectUri out of the state value and sends the code there.

A safe version has one of two properties, and this had neither:

  • state is an opaque random handle that maps to something stored server-side; or
  • the payload is signed, bound to the browser session, short-lived, and single-use.

The attack, start to finish

We ran the full sequence on a controlled test account against live infrastructure.

Step 1. Register a client. The broker's DCR endpoint takes anonymous registrations, so we registered a public client with token_endpoint_auth_method: none and our own redirect URI.

Open dynamic client registration
Figure 4. The broker's registration endpoint hands back a new public client. The returned client ID is redacted.

Step 2. Build the state so it names that client and redirect URI (the JSON above).

Step 3. Ask the broker to start the flow. It hands back a genuine Neon OAuth URL built on Neon's first-party client. The testing client is not anywhere in that URL. It is carried inside state.

Genuine upstream Neon OAuth URL
Figure 5. The broker returns a genuine upstream Neon OAuth URL. The long authorization parameters are redacted.

Step 4. Send the test user the link. They open it, see the real consent screen for their own account, and click Authorize.

Step 5. Neon returns the code to the broker's callback.

Step 6. The broker decodes the state, reads the testing client's redirectUri, and forwards the code there. No listener is required; the code is present in the URL.

Authorization code from the redirected URL
Figure 6. The authorization code pulled straight out of the redirected URL. The code is redacted.

Step 7. We redeem the code as the client we registered in Step 1. The redirect_uri check passes, because that URI really does belong to our client. Out come an access token and a refresh token, scope read write.

Code exchanged for tokens
Figure 7. The intercepted code exchanged for tokens. Access and refresh tokens are redacted.

Step 8. We use the token. GET /api/v2/users/me returns as the test user and provides a database connection URI containing the role password.

users/me confirms test account access
Figure 8. A request to GET /api/v2/users/me confirms the token accesses the test account and returns a database connection URI with the role password. Bearer token and account identifiers redacted.

Vulnerability classification

We call this out because it changes how the issue is triaged. The redirect check worked: the testing client's redirect URI belonged to its registered client, so the match passed. It answered the question it was built to answer. What the check did not address was whether the person who approved Neon's first-party consent screen had agreed to a dynamic downstream client receiving their code. They had not. We classify this as CWE-352 (missing state/session binding) leading to CWE-287 (account takeover). Classifying it solely as an open redirect may not address the missing transaction and session binding.

Impact and credential rotation

Revoking the OAuth grant does not fully remediate this. The token retrieved a database connection string, and that string is a credential in its own right. It keeps working until the password is rotated, grant or no grant. Similar architectures may face additional risk when authorized APIs return durable secondary credentials, such as cloud access keys, kubeconfigs, CI secrets or repository tokens. OAuth revocation closes the grant; it does not invalidate a secret that has already been copied.

Implementation considerations

A remote MCP server usually cannot pre-register every client that might connect (for example Cursor, Claude Code, or an internally built CLI), so it relies on Dynamic Client Registration and effectively acts as an OAuth server itself. To the upstream SaaS provider it is a single static client, so it has to bridge the two flows and track which dynamic client started the request while the user is at the identity provider. Performing that bridging in state rather than server-side is what produces this class of issue.

Related industry research

Obsidian Security documented this MCP OAuth-proxy takeover pattern in January 2026, so it is not new. A May 2026 study by Zhou et al. tested 119 OAuth-enabled MCP servers and reported an authentication flaw in every one, with DCR flaws in 96.6 percent, while 40.55 percent of the wider 7,973 servers they scanned exposed tools with no authentication at all. The 2026-07-28 MCP specification has since deprecated DCR and named the confused-deputy pattern. Existing DCR-based deployments may therefore require separate review.

Recommended remediation

Effective remediation requires several complementary controls.

Fix 1. Stop putting routing authority in unsigned state. Generate a random nonce and store the context server-side (illustrative):

const nonce = crypto.randomBytes(32).toString("base64url");
await store.put(nonce, {
  clientId, redirectUri, scope,
  session: bindToBrowserSession(req),
  createdAt: Date.now(),
  used: false,
}, { ttlSeconds: 600 });
// send `nonce` as the OAuth state, nothing else

On the callback, look the nonce up and reject it if it is missing, expired, already used, or not tied to the same browser session. If data must be carried in state, integrity protection should be combined with session binding, expiration and single-use enforcement.

Fix 2. Bind state to the session after consent, not before. Create the binding once the user approves the MCP-level consent, right before the user is sent upstream. Use __Host- cookies with Secure, HttpOnly, SameSite=Lax, and host-only scope, signed or stored server-side. Make it single-use with a ten-minute lifetime.

Fix 3. Show real per-client consent at the MCP layer. The upstream provider only ever sees the static first-party client, so operators have to run their own consent screen. It should name the registered client, the exact redirect URI that will get the code, the scopes, and the account. Consent should be recorded per user, client, redirect URI and requested scope.

Fix 4. Lock down DCR. Require exact redirect-URI registration and matching. Allow loopback only for local clients and custom schemes only for known native ones, require HTTPS for anything remote, ban wildcards, and rate-limit registration. Prefer Client ID Metadata Documents where the connecting clients can handle it.

Fix 5. Make PKCE mandatory. Reject anything without a code_challenge, reject plain, and enforce S256. Mandatory PKCE with S256 reduces the risk that an intercepted code can be redeemed.

Fix 6. Rotate the secondary secrets during incident response. If a stolen token can reach database credentials, revoking the grant is not the end of it. Revoke affected refresh tokens, rotate database passwords and connection strings, rotate anything downstream the tools could touch, and review the API logs for what was accessed.

Disclosure

We reported the issue through Neon's security program on July 2, 2026, following private coordination. The report was closed as a duplicate of an earlier submission.

We reproduced the behavior described in this article using a controlled test account on the reporting date. We did not test against any third-party account. We have not subsequently re-verified the current broker implementation, so this article does not assert that the issue remains exploitable today.

Sources

  • IETF, RFC 9700: Best Current Practice for OAuth 2.0 Security.
  • IETF, RFC 7636: Proof Key for Code Exchange by OAuth Public Clients.
  • IETF, RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol.
  • IETF, RFC 9207: OAuth 2.0 Authorization Server Issuer Identification.
  • Model Context Protocol, Authorization, specification version 2026-07-28.
  • Model Context Protocol, Security Best Practices, specification version 2026-07-28.
  • Obsidian Security, When MCP Meets OAuth: Common Pitfalls Leading to One-Click Account Takeover, January 29, 2026.
  • Huijun Zhou et al., A First Measurement Study on Authentication Security in Real-World Remote MCP Servers, arXiv:2605.22333, May 21, 2026.
Why Traceforce

Secure your AI attack surface before the breach happens

Get started
Researcher
Author
Abhijeet Kumar
LinkedIn
Traceforce
How Traceforce Works
Understand how Traceforce detects and controls AI risks in real time.
Schedule a Demo
Get Started

Observe and Secure AI at the device layer

Get started
Get started
footer-cube
Footer layerFooter layer
nav logo
Maps and Controls how AI takes action directly on devices
Product
Platform OverviewDocs
Resources
Customer StoriesBlogs & Insight
Company
About UsLegalContact Us
traceforce
© 2026 Traceforce. All rights reserved.
Privacy Policy
bg-texture