Build1 distinct publisher3 min readPublished
A researcher found the SSRF guard sitting on the credential-submission side of an MCP auth SDK while the .well-known discovery fetch ran unchecked from the public entry point, which is the wrong end of the flow to protect.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Discovery runs before trust exists. That is what makes this a structural bug rather than a careless one. An OAuth client has to ask a server where its endpoints live before any token exists and before any issuer has been verified, so the only authority behind that first request is the network position of the process making it. The guard this SDK does have was applied to the part of the flow where credentials are submitted [7]. By then the requests have already gone out.
The chain is short enough to read in one sitting: `auth()` calls `discoverOAuthProtectedResourceMetadata()`, which calls `discoverMetadataWithFallback()`, which calls `tryMetadataDiscovery()`, which calls `fetch()` [4]. Four call boundaries, none of them validating [13]. The check that exists elsewhere is `assertSafeOAuthEndpoint()`, which delegates to `validateDownloadUrl()` [2]. That name is the interesting part: a URL guard written for downloads and later borrowed for OAuth endpoints. Borrowed validation gets applied where it was pasted, not where the data flows, which is the researcher's stated reason for reviewing this class of tooling in the first place [15].
On evidence strength, be precise about what was shown. A mock server on localhost received the SDK's standard OAuth discovery requests, headers and all, after the SDK was pointed at it through its public API [6]. That demonstrates request delivery to `127.0.0.1`. The writeup's other targets, the AWS metadata service at `169.254.169.254`, Redis on 6379, Elasticsearch on 9200 [8], and the credential-theft outcome it describes for instances with an attached IAM role [9], are consequences argued from that primitive rather than captured artifacts [14]. For instance metadata to become stolen credentials, the response body or an error carrying it has to be observable by whoever chose the URL, and that step is not in the writeup [14]. Blind SSRF is still a forged request originating inside your network; the distinction only changes which severity you file it under.
Three things have to hold for the finding to transfer to your deployment. The `serverUrl` has to be influenced by data you do not control, which the writeup notes can be a config file, a database row, or a user-supplied value [12]. The process has to sit somewhere with internal services worth reaching [9]. And nothing between the process and those services can be filtering egress. If your MCP client only ever dials a hardcoded host through a proxy that resolves and allowlists, this is a library defect and not your incident.
The awkward part is that the writeup does not name the SDK, describing it only as a fairly popular AI SDK for MCP authentication [11], so there is no version to diff and no advisory to read. The portable check is a shape rather than a version number: a public API that accepts a URL, a `fetch()` reachable from it, and a guard applied at some of the call sites downstream. The attacker in this model needs no credentials and no privileges [10], which means the interesting question about any agent framework you run is not whether it validates URLs, but whether it validates them at every point where a remote name turns into a socket.
Ranked by verification strength, evidence, and original report placement.
A researcher doing security reviews of AI-related tooling found a server-side request forgery in the OAuth metadata discovery path of an SDK that handles authentication for MCP servers.
The SDK codebase contains a function assertSafeOAuthEndpoint(endpointUrl: URL) that calls validateDownloadUrl(endpointUrl.href).
A separate function responsible for discovering OAuth metadata also fetches URLs during the OAuth flow, and it does not call validateDownloadUrl.
The call chain runs from the public auth() API to discoverOAuthProtectedResourceMetadata(), then discoverMetadataWithFallback(), then tryMetadataDiscovery(), then fetch(), with no validation anywhere in that chain.
The URL that gets fetched comes from the serverUrl the developer supplies when setting up the MCP connection, and the SDK begins making HTTP requests to it to discover OAuth metadata.
The researcher wrote a mock server listening on localhost, pointed the SDK at it through the public API with a serverUrl that validation should have rejected, and the mock server received the standard OAuth discovery requests; the SDK connected to 127.0.0.1 with headers identifying itself.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 29, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
MCP is four trust boundaries, and credentials only close one of them1 distinct publisher
build
Three services you can delete: queue, cache and search in one Postgres1 distinct publisher
build
MCP standardizes the tool call, not the authority to cause the effect1 distinct publisher
build
Microsoft ships an MIT-licensed agent kernel: policy rings, Ed25519 identity, kill switch1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Precise on mechanism, silent on the target
Everything here comes off one person's screen, and it is unusually specific for that: a named guard function, a four-hop path from the public entry point to a raw fetch, and a localhost server that logged the discovery requests arriving. The problem is the one detail withheld. Without the package name, an argument built entirely from function names cannot be checked by anyone who reads it.
No install base in evidence
We are told the SDK is 'fairly popular' and that is the entirety of the exposure data. No name, no version, no download figure, no dependent projects, and the only deployment in the piece is the researcher's own mock listener on 127.0.0.1. The disclosure-and-fix note is second-hand from the finder, undated and unconfirmed, which tells us nothing about how much running code was ever affected.
Impact runs a step ahead of the demo
What the test produced was a request arriving at localhost. What the writeup describes is IAM credential theft from the AWS metadata service. Reaching an address and lifting a role's credentials from it are different results, and only the first one is on the page. The overshoot is modest rather than cynical — the pattern is real and the finder says so plainly — and it is partly offset by how little space the redirect-follow default gets, which is the finding that makes a naive patch insufficient.
Self-published finder, self-graded severity
The researcher is also the reporter, the publisher and the beneficiary. This is a reputation post on dev.to, structured as a story about the bug that almost got away, and its thesis — AI tooling ships too fast to validate anything — is the author's stated reason for looking there in the first place. Withholding the SDK's name is defensible if the fix is fresh, but it also removes the one fact that would let a reader grade the claim, and severity here is assigned by the person whose finding it is.
Familiar pattern, unconfirmed instance
Validate one fetch, forget the sibling that also fetches, then follow redirects anyway: this is among the most common ways SSRF survives a review, so the shape is easy to believe. Believing the shape is not confirming the instance. There is no maintainer acknowledgement, no identifier, no second account, and the reproduction that exists stops at the first hop.