Skip to content

Lab: Close enough (OAuth redirect_uri validation)

Difficulty: medium. Time: 20 to 40 minutes. Run it: node redirect-prefix.mjs (lab folder), then open http://localhost:4102/.

A notes app is a public OAuth client with no PKCE. Its registered redirect URI is http://localhost:4102/cb. A victim is already signed in at the identity provider and opens any link you send them (POST /victim/visit). An attacker listener on 127.0.0.1:4103 records every request it receives.

Goal: get the victim’s authorization code, exchange it at /token, and read the victim’s note from /api/me.

Hint 1

Ask the server which redirect_uri values it accepts. Try values that are not the registered one and read the error text.

Hint 2

The check compares the beginning of the string. A URL is not just a string: which part of it decides where a browser connects?

Redirect URI matching must be exact. A “starts with our origin” or “contains our domain” check lets an attacker build a URL that passes the check but sends the code to a host they control. Strict matching is also why AADSTS50011 exists: the annoying error is the protection working.

Solution and fix (spoilers)

Use http://localhost:[email protected]:4103/steal as the redirect_uri. It starts with the allowed origin, but everything before the @ is user info, so the browser connects to 127.0.0.1:4103. Send the victim the /authorize link, read the code from the listener’s /log, exchange it at /token, then call /api/me.

The fix is in the --fixed build: compare the whole URI for equality against the registered value. PKCE would also have made a stolen code useless on its own.

Failed redirect URI matches show up as AADSTS50011 in the sign-in logs. A burst of them for one application from one IP is someone probing the matching rule; see the KQL on the AADSTS50011 page.