Skip to content

Security: /auth/authorize validates the request before authenticating, so an unauthenticated user agent can be redirected to a client's URI #213

Description

@leodip

Summary

RFC 9700 section 4.11.2 carries a MUST that this server does not currently satisfy:

The authorization server MUST take precautions to prevent these threats. The authorization server MUST always authenticate the user first and, with the exception of the silent authentication use case, prompt the user for credentials when needed, before redirecting the user.

HandleAuthorizeGet in src/authserver/internal/handlers/handler_authorize.go calls authorizeValidator.ValidateRequest and then authorizeValidator.ValidateScopes. Both deliver their errors to the client by redirecting to the registered redirect URI, and both run before the session check and before any redirect to /auth/level1. So a link carrying an invalid scope redirects a logged-out visitor to a host the client chose, with nothing rendered and nobody authenticated.

Severity: Low to Medium, depending on how a client's redirect URI came to be registered. See "What bounds it" below.

Reproduction

Verified against a running server. A browser holding no cookies, sending an invalid scope for a client whose redirect URI is https://attacker.example.com/callback:

GET /auth/authorize/?client_id=<client>&redirect_uri=https%3A%2F%2Fattacker.example.com%2Fcallback
    &response_type=code&code_challenge_method=S256&code_challenge=...
    &scope=openid+nonsense&state=probe&nonce=...
302 Found
Location: https://attacker.example.com/callback?error=invalid_scope&error_description=Invalid+scope+format%3A+%27nonsense%27....&state=probe

Same result for an unknown resource:permission:

Location: https://attacker.example.com/callback?error=invalid_scope&error_description=Invalid+scope%3A+%27nosuchresource%3Anosuchperm%27....&state=probe

No login page, no consent screen, no user interaction beyond loading the URL.

Why the ordering matters

This is the first of the three attacks RFC 9700 section 4.11.2 describes for an authorization server used as a redirector:

  1. Intentionally send an erroneous authorization request, e.g., by using an invalid scope value, thus instructing the authorization server to redirect the user agent to its phishing site.

The other two are a declined consent (the server redirects anyway, regardless of what the user chose) and a prompt=none request. The spec's remedy for the first is the ordering: authenticate before redirecting, so an anonymous drive-by cannot use the endpoint at all.

What bounds it

Stating these plainly, since they are why this is not High:

  • The redirect URI must be registered on that client, so this is only interesting where an attacker can get one registered. With dynamic client registration disabled (the default) that means persuading an administrator.
  • The destination is the client's exact, pre-registered redirect URI. Matching is plain string equality in src/core/validators/authorize_validator.go, with no wildcards or prefixes, so this does not let anyone reach an arbitrary URL.
  • The response carries only an OAuth error, no code and no token.

The value to an attacker is the redirect itself: a link that appears to point at the authorization server's own domain and lands somewhere else, which is the phishing property RFC 9700 is describing.

Relationship to #108

#108 mitigates the consequence for one class of client without changing the ordering. It adds an interstitial for clients created through dynamic client registration: rather than redirecting, the server shows the user where the client wants to send them and lets them choose. That is RFC 9700's SHOULD-level answer:

The authorization server SHOULD only automatically redirect the user agent if it trusts the redirection URI. If the URI is not trusted, the authorization server MAY inform the user and rely on the user to make the correct decision.

The MUST quoted at the top asks for something different and broader: that no unauthenticated user is redirected at all, for any client, self-registered or not. #108 leaves that untouched, deliberately.

Suggested fix

Move request and scope validation to after authentication in HandleAuthorizeGet, so the flow becomes:

  1. validate client_id and redirect_uri (unchanged, and already required to come first by RFC 6749 4.1.2.1, which says the server MUST NOT redirect to an invalid redirect URI)
  2. authenticate the user
  3. validate the rest of the request and its scopes
  4. deliver any error by redirect

prompt=none keeps its current ordering, per the silent authentication carve-out in the sentence quoted above and per OIDC Core 3.1.2.1.

Worth sizing carefully before starting. /auth/authorize is the busiest path in the server, and a large number of existing tests assert the current ordering by driving a request that fails validation and expecting a redirect. Expect the change to be mostly test churn rather than handler code, and expect at least one case where an error currently reported to the client would now require a login first, which is a behaviour change for clients that rely on fast failure.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggoPull requests that update Go codesecuritySecurity issue or hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions