Skip to content

[TAN-8575] Restrict OAuth redirect_uri to http(s) at registration, validation and navigation - #14700

Open
Stef-Rousset wants to merge 6 commits into
masterfrom
TAN-8575-redirect-uri-fix
Open

[TAN-8575] Restrict OAuth redirect_uri to http(s) at registration, validation and navigation#14700
Stef-Rousset wants to merge 6 commits into
masterfrom
TAN-8575-redirect-uri-fix

Conversation

@Stef-Rousset

@Stef-Rousset Stef-Rousset commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This PR has been written with Claude code.

🎩 What? Why?
Our Dynamic Client Registration endpoint (POST /oauth/registrations) lets any client register itself with a redirect_uri of its own choosing, and the consent screen later navigates the browser to that URI. Nothing constrained the scheme, so a client could register javascript:..., data:... or vbscript:... and have it executed in the platform origin the moment a user approved (or denied) the consent screen — the stored URI was handed straight to window.location.assign.

This PR closes that with three independent layers, so no single code path is the only thing standing between an attacker-supplied string and the browser:

1. Registration endpoint (Oauth::RegistrationsController)
redirect_uris are now validated before the application is built: the list must be non-empty and every entry must be an absolute http(s) URI with a host and no fragment (RFC 6749 §3.1.2). Rejections return invalid_redirect_uri / 400 and persist nothing. The failure path of application.save now also distinguishes invalid_redirect_uri from the generic invalid_client_metadata, so clients get the error code the spec expects.

2. Doorkeeper configuration (config/initializers/doorkeeper.rb)
forbid_redirect_uri is enabled with a scheme allowlist (http, https), so the rule holds for any code path that creates a Doorkeeper::Application, not just our controller. force_ssl_in_redirect_uri keeps its existing job — deciding whether plain http is acceptable — and now reads uri.hostname instead of uri.host, which fixes two things: IPv6 loopback (http://[::1]:33418/cb, where host includes the brackets) and uppercase hostnames. Loopback http callbacks stay valid because that is what MCP clients register (RFC 8252).

3. Consent screen (front/app/containers/OAuthAuthorize)
The scheme is re-checked in the browser before any navigation. If the redirect_uri returned by the API is not http(s), the consent screen refuses to render at all and shows the error card, rather than only blocking the final navigation. Both exits — approve and deny — go through a single navigateToClient choke point, so window.location never receives a URL whose scheme we have not validated ourselves. The navigation itself is extracted to utils.ts (navigateToUrl) purely so the behaviour is assertable: jsdom exposes window.location as read-only and it cannot be spied on in place.

✅ Tasks

Add tests:

  • back/spec/config/doorkeeper_redirect_uri_spec.rb — table-driven validation of the Doorkeeper rules: https accepted, http accepted only on loopback (hostname, IPv4, IPv6, uppercase), and javascript: / data: / vbscript: refused, including the javascript://x%0A… authority-shaped variant and a multi-line redirect_uri where a valid https entry is followed by a hostile one.
  • back/spec/requests/oauth_registrations_spec.rb — registration succeeds for https, loopback http and multiple URIs; hostile schemes are rejected with invalid_redirect_uri and create no application; non-redirect_uri failures still report invalid_client_metadata.
  • front/app/containers/OAuthAuthorize/utils.test.ts and index.test.tsx — the helper accepts https/loopback http and rejects script-bearing schemes and non-absolute URLs; the consent screen renders and redirects normally for http(s), refuses to render for a hostile redirect_uri, and navigates nowhere if a hostile URI comes back from the approve call.

Changelog

Technical

  • OAuth client registration and the consent screen now only accept http(s) redirect URIs, blocking script-bearing schemes (javascript:, data:, vbscript:) from being stored or navigated to.
  • OAuth redirect URI validation now handles IPv6 loopback and uppercase hostnames correctly.

@notion-workspace

Copy link
Copy Markdown

@Stef-Rousset
Stef-Rousset marked this pull request as ready for review September 1, 2026 12:17
@Stef-Rousset

Copy link
Copy Markdown
Contributor Author

@jamesspeake , I would also like to have your opinion 🙏 on "Claude re-evaluation about unauthenticated Dynamic Client Registration" that I asked it to perform (result is pasted at the end of the ticket), to see if complementary changes are needed. Thx !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant