Safeguard
Application Security•

ASVS 5.0 V10.4.1: Exact Match Redirect URIs in OAuth

ASVS V10.4.1 (Level 1): the authorization server validates redirect URIs against a pre-registered per-client allowlist using exact string comparison.

Safeguard Security Team
3 min read

ASVS 5.0 V10.4.1 requires the authorization server to validate redirect URIs against a client specific allowlist of pre-registered URIs, using exact string comparison. It is a Level 1 requirement. No wildcards, no prefix matching, no pattern tricks.

Why it matters

After login, the authorization server sends the user back to the redirect URI with a code (or token) attached. If an attacker can make the server redirect to a URL they control, they receive the code. Loose matching is how that happens: a pattern like https://app.example.com/* also matches an open redirect on that host, and a prefix check passes https://app.example.com.evil.test. Real account takeovers have come from exactly these gaps.

How to implement it

Register full URIs per client and compare the whole string.

const REGISTERED: Record<string, readonly string[]> = {
  "web-app": ["https://app.example.com/oauth/callback"],
  "mobile":  ["com.example.app:/oauth/callback"],
};

export function redirectAllowed(clientId: string, redirectUri: string | undefined) {
  const list = REGISTERED[clientId];
  if (!list || typeof redirectUri !== "string") return false;
  return list.includes(redirectUri);          // exact string equality only
}

// in the authorize handler
if (!redirectAllowed(req.query.client_id, req.query.redirect_uri)) {
  return res.status(400).send("invalid redirect_uri"); // never redirect to it
}

Important behaviors:

  1. If the URI is invalid, show an error page. Do not redirect to the bad URI to report the error.
  2. Do not normalize, decode or lowercase before comparing, and do not strip query strings or fragments to make a match.
  3. Registered URIs use HTTPS (or a claimed scheme for native apps). Plain HTTP only for loopback during development.
  4. Do not register wildcards or whole domains. If a client needs several callbacks, list each.
  5. Remove stale and test URIs from production registrations during access reviews.
  6. Require the same redirect URI in the token request, as the spec says.

On the client side, also fix open redirects in your own app, since a registered callback path that redirects onward is still a leak path.

How to test it

Try each of these as redirect_uri against the authorize endpoint for a real client and confirm every one is refused with an error page and no redirect:

  • https://app.example.com/oauth/callback/../evil
  • https://app.example.com/oauth/callback.evil.test
  • https://app.example.com.evil.test/oauth/callback
  • https://app.example.com/oauth/callback?next=https://evil.test
  • https://app.example.com/oauth/callback#x
  • http://app.example.com/oauth/callback
  • the same path with different case or a trailing slash

Then confirm the exact registered string works. Add these cases to an automated suite and run them in CI. Static analysis can catch regex based matching in your authorization server code, and the SAST demo shows how such findings appear.

Common mistakes

  • Wildcard or regex based registrations.
  • startsWith or endsWith comparisons.
  • Accepting a redirect URI the client never registered when the parameter is optional.
  • Redirecting to an invalid URI with an error code.
  • Letting self-service client registration accept any URI without review.

Try it yourself

Click through it at your own pace, or open it full screen.

Quick check

Why is a prefix match unsafe?

A prefix such as https://app.example.com also matches https://app.example.com.evil.test, sending codes to an attacker.

The redirect URI is invalid. Where should the user go?

To an error page on the authorization server. Never redirect to the unverified URI.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.