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.

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.

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.

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>"
}
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:
stateis 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.

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.

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.

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.

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.

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 elseOn 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.

.webp)
.webp)

