
Cloud Vulnerability DB
A community-led vulnerabilities database
This issue is reachable only when Dynamic Client Registration is enabled:
Doorkeeper::OpenidConnect.configure sets dynamic_client_registration true (the routes are not mounted otherwise), andauthorize_dynamic_client_registration is left unconfigured — its default is nil, which lets any unauthenticated caller register a client.
DCR is not enabled by default, so a deployment that never turned it on is not affected.
If DCR is enabled but registration is gated by authorize_dynamic_client_registration, the issue is still reachable by anyone that gate admits: the escalation happens during registration itself, not before it. The gate reduces who can do it, it does not prevent it.
enforce_configured_scopes does not mitigate this issue. It was listed among the preconditions of the earlier, related advisory (GHSA-8r7r-wh7x-27ff / CVE-2026-70665, which required it to be off), so it is natural to expect it to help here. It does not: an application whose scope set is empty trivially satisfies "is a subset of the configured scopes", so the validation passes and the escalation proceeds unchanged. This was verified with enforce_configured_scopes enabled.In doorkeeper-openid_connect >= 1.9.0, < 2.0.0, a Dynamic Client Registration request that omits the scope parameter registers a client whose scope set is empty. Doorkeeper treats an application with no scopes as unrestricted, so such a client may subsequently request every scope the authorization server defines — including scopes the server never intended to expose to dynamically registered clients.
A client that honestly declares a narrow scope is confined to it. A client that declares nothing is not confined at all. Omitting a parameter yields strictly more privilege than supplying it.
DynamicRegistrationRequest#validate_scope rejects a registration whose requested scopes are all unsupported, but returns early — accepting the registration — when no scope was requested at all:
# lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb
def validate_scope
return true if requested_scopes.blank? || permitted_scopes.present?
# ...
endThe resulting application is persisted with scopes set to the empty string. Doorkeeper's scope validation then falls back to the full server scope set for any application whose own scope set is blank:
# doorkeeper: lib/doorkeeper/oauth/helpers/scope_checker.rb
def valid_scopes(server_scopes, app_scopes)
app_scopes.presence || server_scopes
endso every scope check that application ever undergoes is evaluated against default_scopes + optional_scopes rather than against anything it registered.
The permitted_scopes documentation in the same file already identifies this hazard — "persisting an empty scope set would make Doorkeeper's ScopeChecker fall back to the full server scope set" — and validate_scope is the guard written to prevent it. The omitted-scope path bypasses that guard.
Server configuration: default_scopes :public, optional_scopes :openid, :profile, :admin, dynamic_client_registration true, authorize_dynamic_client_registration unset. No resource owner is involved at any point.
Step 1 — register with scope omitted, requesting only the client credentials grant:
POST /oauth/registration
Content-Type: application/x-www-form-urlencoded
client_name=attacker&redirect_uris[]=https://attacker.example/cb&grant_types[]=client_credentials201 Created
{"client_id":"...","client_secret":"...","scope":"", ...}The persisted application has scopes = "".
Step 2 — request a scope that was never registered:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=...&client_secret=...&scope=admin200 OK
{"access_token":"...","token_type":"Bearer","expires_in":7200,"scope":"admin","created_at":...}Control — the same flow with scope=public declared at registration:
400 Bad Request
{"error":"invalid_scope","error_description":"The requested scope is invalid, unknown, or malformed."}The client_credentials grant is used so the chain completes without any consent screen. The same registered client is equally unrestricted in the authorization_code flow, where it can present every server scope on the consent screen.
That advisory covered the neighbouring path — a DCR request that supplies scopes the server does not configure — and was fixed in 1.10.4 by adding validate_scope. The fix is incomplete: it validates supplied scopes but leaves the omitted-scope path untouched, and that path is reachable in every release since DCR was introduced, including the versions the earlier advisory lists as patched.
The omitted-scope outcome is also broader than the one that was fixed. The earlier issue let a client claim the scopes it named; this one gives it every scope the server defines, without naming any.
| Versions | scope supplied but unsupported | scope omitted |
|---|---|---|
>= 1.9.0, <= 1.10.3 | persisted verbatim (CVE-2026-70665) | empty scope set → every server scope |
>= 1.10.4, < 2.0.0 (incl. 1.10.5 and 2.0.0.beta1) | rejected with invalid_client_metadata | empty scope set → every server scope |
An actor able to reach the registration endpoint obtains a client credential entitled to every scope the authorization server defines. Where the server defines administrative or otherwise privileged scopes, that is a direct privilege escalation, and with the client_credentials grant it requires no resource owner interaction. The registered client is indistinguishable from a legitimately registered one in the applications table apart from its blank scopes column.
Fixed in 2.0.0. Upgrade to >= 2.0.0; a registration request that omits scope is then registered with the server's default_scopes instead of an empty scope set (doorkeeper-gem/doorkeeper-openid_connect@e7ca1de).
Upgrading does not rewrite existing registrations. Any oauth_applications row with a blank scopes value stays unrestricted after the upgrade — apply the backfill step under Workarounds.
No backport to the 1.9.x–1.10.x series is planned. Deployments that cannot take the 2.0.0 upgrade yet should apply one of the workarounds below.
If you cannot upgrade immediately, any of the following prevents the issue:
authorize_dynamic_client_registration and reject requests that carry no scope, ordynamic_client_registration false, the default), oroauth_applications row with a blank scopes value is currently unrestricted:Doorkeeper::Application.where(scopes: [nil, ""]).find_each do |app|
app.update!(scopes: Doorkeeper.config.default_scopes.to_s)
endReview those rows before overwriting them — the same blank-scope condition also applies to applications created outside DCR (e.g. through the admin UI), where it may have been relied on deliberately.Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."