Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Redirect URI validation

An authorization code goes wherever the authorization request asks for it, as long as the authorization server agrees. The printer writes that request, but anyone can write one. It is an ordinary URL, and nothing stops an attacker from copying the printer's client ID into a request of their own and naming a different return address.

Redirects and authorization codes said the photo service compares the requested redirect URI with the registered one exactly, refusing even an added trailing slash. That strictness can look fussy until you follow an attacker through a version of the photo service that compares less carefully.

Sending the code somewhere else

All domains and codes in these examples are fictional. The attacker begins by building a link to the photo service's authorization endpoint:

https://auth.photos.example/authorize
  ?response_type=code
  &client_id=photo-printer
  &redirect_uri=https%3A%2F%2Fprinter.example.attacker.example%2Foauth%2Fcallback
  &scope=photos.read
  &state=demo-attacker-state-1
  &code_challenge=13TUFxsJi_vyUQcynxTDC61Vw6HLxqvpa2jIzPC_fBc
  &code_challenge_method=S256

Everything except the redirect URI is what the printer itself would send. The challenge is one the attacker computed from a verifier of their own.

  1. The attacker sends you the link, dressed up as an offer: "Print your summer album free this week."
  2. You are already signed in at the photo service. The consent screen names Photo Printer, because the request carries the printer's client ID, and you approve.
  3. This careless version of the photo service checks the redirect URI with a shortcut: does it begin with https://printer.example? It does, so the service sends your browser, with a fresh code, to printer.example.attacker.example, a name under the attacker's domain.
  4. The attacker's server reads the code from the incoming request.

What the code is worth depends on the client it was issued to. Had the request named the printer's phone app, photo-printer-app, the attacker could redeem the code at once. A public client sends only its client ID to the token endpoint, and PKCE offers no protection here, because the attacker wrote the request and holds the verifier behind its challenge. The printer's website is a confidential client, so the attacker cannot redeem the code without the printer's credentials. They can still try to make the printer redeem it for them.

A consent screen that shows the return domain gives you a slim chance to notice. The address begins with printer.example precisely so that it passes a glance.

How careless matching goes wrong

The prefix check in step 3 is one of several shortcuts that have let attackers through. In each row below, the printer registered only https://printer.example/oauth/callback, except where noted.

Requested redirect URIShortcut that accepts itWhat the shortcut missed
https://printer.example.attacker.example/oauth/callbackBegins with https://printer.example.The host name keeps going. This host belongs to attacker.example.
https://printer.example/oauth/callback/../../goBegins with the registered address.The browser resolves each .. segment, also when written %2e%2e, and arrives at https://printer.example/go.
https://printer.example/oauth/callback/previewThe registered address or anything below it.A different page, built for a different purpose, receives the code.
https://printer.example/oauth/callback?next=https://attacker.example/Compares the address without its query.The callback receives an instruction the printer never registered, which a callback that follows next would obey.
https://attacker.example\.printer.example/oauth/callbackThe host, as read by the server's own parser, ends with .printer.example.Browsers treat \ as / in web addresses, so the host is attacker.example.
https://promo.printer.example/oauth/callback, with https://*.printer.example/oauth/callback registeredAny subdomain of printer.example.The subdomain may no longer be under the printing company's control.
http://printer.example:8080/oauth/callbackThe port may vary and plain HTTP is allowed, rules meant for loopback addresses.A different service on another port receives the code over an unencrypted connection.

The wildcard row needs one more idea. A subdomain takeover happens when a DNS record still points a name such as promo.printer.example at an outside hosting service the printing company has stopped using. Anyone who opens an account with that service under the old name can then serve pages at the printer's subdomain, and a wildcard registration would send them codes.

The backslash row belongs to a family of tricks that work because the validator and the browser read the same text differently. Percent-encoding, unusual characters, and letter case all give two parsers a chance to disagree. Exact comparison avoids the question. The requested value must be the same sequence of characters as a registered value, so even https://PRINTER.example/oauth/callback is refused, although it leads to the same place. Refusing a harmless variant costs a developer one corrected setting.

The only flexibility current guidance allows is the one Redirect URIs and client metadata described: the port of a loopback address, such as 127.0.0.1, used by a native app. It does not extend to any other host.

Leaks from the printer's own pages

Suppose the printer's site has a page at https://printer.example/go that sends the browser to whatever address appears in its to parameter, used for "continue to our partner" links. That is an open redirect, the kind of page Correlating requests and responses warned against.

If the photo service accepted any address on printer.example, the attacker could ask for the code to be returned to /go, with to pointing at attacker.example. The validation passes, because the address really is on the printer's domain, and the printer's own page then forwards the browser onward. Whether a code in the query survives the second hop depends on the redirector: some pass the remaining parameters along, and one that redirects with script can expose its full address in the Referer header if its referrer policy allows it. A response carried in the URL fragment, as the implicit grant carried access tokens, fares worse. When an HTTP redirect names no fragment of its own, the browser carries the original fragment along to the new address. Exact matching closes this route, because /go is not a registered address, and current guidance goes further: clients and authorization servers must not run open redirectors at all.

Even with exact matching, the code still arrives at a page, and that page can leak it. Redirects and authorization codes asked for a callback without third-party resources, and the reasons are concrete. Any script running on the callback page can read the full address, so an analytics or advertising script there would receive every code. Requests the page makes to other sites carry a Referer header. Current browsers send only the site's origin to other sites by default, but a page can choose a policy that sends the full address. The printer's callback therefore loads nothing from other sites, sets its own referrer policy, exchanges the code, and moves the browser to a clean address straight away:

HTTP/1.1 303 See Other
Location: https://printer.example/photo-book
Referrer-Policy: no-referrer
Cache-Control: no-store

The code's own properties help as well. It is single-use and short-lived, so a copy that reaches someone after the printer has redeemed it is worth very little.

Who checks what

Both sides share the work, and neither can do the other's part.

PartyResponsibility
Authorization serverCompares the requested redirect URI with each registered value as an exact string, before showing anything or redirecting. The only exception is a loopback port for a native app.
Authorization serverOn a mismatch or an unknown client, shows an error on its own page and never redirects, as Errors and denied access described.
Authorization serverRefuses redirect URIs that use plain http, apart from a native app's loopback address, and refuses wildcard registrations. A native app's private-use scheme remains allowed.
Authorization serverStores the redirect URI with the code and checks the token request against it.
ClientRegisters each exact callback, with test sites under their own registrations.
ClientRuns no open redirects on its domains and validates any return destination after the callback.
ClientKeeps the callback free of third-party content, sets a strict referrer policy, and moves to a clean address.
ClientRemoves DNS records for services it no longer uses.

The authorization server's check decides where a code can go, and the client's care decides whether it stays there. When both fail and a code for the printer's website escapes, the attacker still cannot redeem it without the printer's credentials. Code interception and injection follows what they do instead.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2The photo service accepts any redirect URI that begins with https://printer.example. What can an attacker do?

QUESTION 2 OF 2A careless matcher sends a code for the printer's public phone app to an address the attacker chose. The attacker wrote that authorization request. Why does PKCE not protect the code?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity