[TAN-8575] Restrict OAuth redirect_uri to http(s) at registration, validation and navigation - #14700
Open
Stef-Rousset wants to merge 6 commits into
Open
[TAN-8575] Restrict OAuth redirect_uri to http(s) at registration, validation and navigation#14700Stef-Rousset wants to merge 6 commits into
Stef-Rousset wants to merge 6 commits into
Conversation
Stef-Rousset
marked this pull request as ready for review
September 1, 2026 12:17
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 ! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Changelog
Technical
http(s)redirect URIs, blocking script-bearing schemes (javascript:,data:,vbscript:) from being stored or navigated to.