Page MenuHomePhabricator

CVE-2025-32068: Revoking authorization of OAuth2 consumer does not invalidate refresh tokens
Closed, ResolvedPublicSecurity

Description

Steps to replicate the issue:

  • Create OAuth2 consumer (not owner-only) with some grants like edit.
  • Authorize the consumer, store access and refresh token
  • Revoke authorization of the consumer using Special:OAuthManageMyGrants
  • Try to edit the wiki using the stored access token. It fails with below error:
{
  "code": "mwoauth-invalid-authorization",
  "text": "The authorization headers in your request are not valid: Invalid access token",
}
  • Get a new access token through rest.php/oauth2/access_token using the stored refresh token. You successfully receive a new refresh and access token.
  • Try to edit the wiki using the new access token. It fails with below error:
{
  "code": "mwoauth-invalid-authorization",
  "text": "The authorization headers in your request are not valid: Cannot create access token, user did not approve issuing this access token",
}

What happens?:
The OAuth2 consumer is able to get new access tokens despite the authorization being revoked.
Both errors when using the access tokens having the same error code also makes it very hard to differentiate between an invalid token and revoked authorization.

What should have happened instead?:
The request to rest.php/oauth2/access_token should have returned an error due to the consumers authorization being revoked. (Invalid refresh token)
See also https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/

Keep in mind that at any point the user can revoke an application , so your application needs to be able to handle the case when using the refresh token also fails. At that point, you will need to prompt the user for authorization again, beginning a new OAuth flow from scratch.

Software version:
MediaWiki 1.39.3 (469b9cb)
OAuth 1.1.0 (28d55e5)

Other information:
Used OAuth2 consumer: https://wikibot.miraheze.org/wiki/Special:OAuthListConsumers/view/f7d7b6d85767eb1b443f283b63d46d5b
Access tokens are removed at Control/ConsumerAcceptanceSubmitControl.php#L223, however refresh tokens stay unchanged.

Event Timeline

Restricted Application added subscribers: Reception123, Aklapper. · View Herald Transcript
MarkusRost set Security to Software security bug.
MarkusRost changed the visibility from "Public (No Login Required)" to "Custom Policy".
MarkusRost changed the subtype of this task from "Bug Report" to "Security Issue".

While I can't see a clear abuse vector due to the Cannot create access token, user did not approve issuing this access token error, this still feels a lot like a security issue. Therefore I'm escalating just to be sure.

mmartorana changed the task status from Open to In Progress.May 11 2023, 2:26 PM
mmartorana triaged this task as Medium priority.
mmartorana added a project: Vuln-Authn/Session.
mmartorana changed Risk Rating from N/A to Medium.

I don't think there's a straightforward way of doing this. RefreshTokenRepository is a CacheRepository subclass so you can only delete if you know the token ID. We'd need to be able to delete by acceptance ID.

Hello Platform Engineering - would any of you be interested in addressing this matter?

@ItSpiderman - Considering your contributions, are you open to taking on this task? Appreciated.

In T336113#8983811, @Tgr hat geschrieben:

I don't think there's a straightforward way of doing this. RefreshTokenRepository is a CacheRepository subclass so you can only delete if you know the token ID. We'd need to be able to delete by acceptance ID.

If it's not easily possible to invalidate the refresh token, would it at least be possible to provide a slightly different error code when using the access token? That way it would be at least possible to identify the approval as revoked.

Security-Team I have found an abuse vector for this issue. While the new access tokens are invalid for editing the wiki, they are still valid for the identify endpoint oauth2/resource/profile.

Deauthorizing an identify-only consumer has absolutely no effect, the consumer is able to continue using the refresh token to get new valid access and refresh tokens. Allowing them to continue to access your profile (I haven't tested with email/realname identify consumer). In fact, the indentify-only consumer will have absolutely no indication at all about their access having been revoked.

I have just tested it and this issue exists for mwoauth-authonlyprivate as well, allowing the consumer to keep accessing my email address without me being aware of it or having any way to prevent it.

Tgr raised the priority of this task from Medium to Unbreak Now!.Jan 21 2025, 3:29 PM

Deauthorizing an identify-only consumer has absolutely no effect, the consumer is able to continue using the refresh token to get new valid access and refresh tokens.

Thanks for checking, that sounds bad.

If it's not easily possible to invalidate the refresh token, would it at least be possible to provide a slightly different error code when using the access token?

That makes sense, regardless of this issue. Should probably be a separate task though.

Tgr lowered the priority of this task from Unbreak Now! to High.Jan 21 2025, 3:48 PM
Tgr added subscribers: polishdeveloper, JTweed-WMF.

RefreshTokenRepository is a CacheRepository subclass so you can only delete if you know the token ID.

More generally, access tokens are stored in the database but refresh tokens are stored in the cache and so aren't findable without knowing the refresh token id. It would be good to know the thinking behind that.

Verifying refresh tokens happens via RefreshTokenRepository::isRefreshTokenRevoked(). The refresh token contains the access token ID; the access token is stored in the DB, and seemingly only deleted when the user revokes access. (Really? That would mean that expired access tokens pile up forever. Seems weird but production has 1.5M rows and the oldest has an expiry in 2020, so probably true?) That means we can use the existence of the access token row as a way to check whether the acceptance has been revoked. It's not ideal (the refresh token should contain a reference the oauth_accepted_consumer row instead) but seems easy enough to do as a security patch, at the cost of adding a DB query to every refresh token revocation check (which should be fine, by that point several queries should have been needed already).

Test plan:

  • Create some OAuth 2 consumer; confidential, not owner-only, default grants. Note down the client ID and secret.
  • Visit <wiki>/w/rest.php/oauth2/authorize?response_type=code&client_id=<client id> and authorize. Note down the code URL parameter at the end of the redirect.
  • curl -X POST -d '<wiki>/w/rest.php/oauth2?grant_type=authorization_code&code=<code>&client_id=<client id>&client_secret=<client secret>' '<wiki>/w/rest.php/oauth2/access_token' Note down the access token and refresh token.
  • Test that the access token works with curl -H 'Authorization: Bearer <access token>' <wiki>/w/rest.php/oauth2/resource/profile
  • Test that the refresh token works with curl -X POST -d 'grant_type=refresh_token&refresh_token=<refresh token>&client_id=<client id>&client_secret=<client secret>' '<wiki>/w/rest.php/oauth2/access_token' (you get a new access token and refresh token; the old ones are revoked)
  • Visit Special:OAuthManageMyGrants and revoke the grant
  • curl -X POST -d 'grant_type=refresh_token&refresh_token=<refresh token>&client_id=<client id>&client_secret=<client secret>' '<wiki>/w/rest.php/oauth2/access_token' should not work anymore with the patch applied.

CR+1. Would be nice to have someone who's a little more familiar w/ ext:OAuth review this as well, prior to security-deployment (@Reedy?)

+1 from me as well. The code change looks correct and I reproduced the bug and the fix locally with the provided test plan (note minor typo: oauth2grant_type should be oauth2&grant_type).

The typo should actually be rest.php/oauth2?grant_type=

Let's plan to get this deployed to Wikimedia production during the next security deployment window (2025-02-24).

This issue is due to be released in about a week-and-a-half. Given that we're still patched in Wikimedia production and that this issue will be made public soon anyways, I think it's fine to merge/backport in gerrit now if you'd like.

Change #1133540 had a related patch set uploaded (by Mstyles; author: Gergő Tisza):

[mediawiki/extensions/OAuth@master] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133540

Change #1133543 had a related patch set uploaded (by Mstyles; author: Gergő Tisza):

[mediawiki/extensions/OAuth@REL1_42] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133543

Change #1133544 had a related patch set uploaded (by Mstyles; author: Gergő Tisza):

[mediawiki/extensions/OAuth@REL1_43] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133544

Change #1133545 had a related patch set uploaded (by Mstyles; author: Gergő Tisza):

[mediawiki/extensions/OAuth@REL1_39] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133545

Change #1133540 merged by jenkins-bot:

[mediawiki/extensions/OAuth@master] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133540

backports out, if someone could merge them, that would be great

backports out, if someone could merge them, that would be great

Done.

Change #1133545 merged by jenkins-bot:

[mediawiki/extensions/OAuth@REL1_39] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133545

Change #1133543 merged by jenkins-bot:

[mediawiki/extensions/OAuth@REL1_42] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133543

Change #1133544 merged by jenkins-bot:

[mediawiki/extensions/OAuth@REL1_43] Check revocation when using refresh tokens

https://gerrit.wikimedia.org/r/1133544

Mstyles renamed this task from Revoking authorization of OAuth2 consumer does not invalidate refresh tokens to CVE-2025-32068: Revoking authorization of OAuth2 consumer does not invalidate refresh tokens.Apr 11 2025, 5:07 PM
Mstyles closed this task as Resolved.
Mstyles changed the visibility from "Custom Policy" to "Public (No Login Required)".