From d5fe5b49df5ee08196be750467deda21917e2b50 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 21 Sep 2026 12:25:23 +0200 Subject: [PATCH] [SECURITY] OAuth: Disable existing tokens when deactivating Application (CVE-2026-101271, Z#23247296) --- src/pretix/api/models.py | 4 +++ src/tests/api/test_oauth.py | 58 +++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/pretix/api/models.py b/src/pretix/api/models.py index 36fda88593..20a5308fc2 100644 --- a/src/pretix/api/models.py +++ b/src/pretix/api/models.py @@ -101,6 +101,10 @@ class OAuthAccessToken(AbstractAccessToken): self.expires = now() - timedelta(hours=1) self.save(update_fields=['expires']) + def is_valid(self, scopes=None): + # Can maybe be removed after upgrading django-oauth-toolkit to 3.4.1 + return super().is_valid(scopes) and self.application.is_usable(None) + class OAuthRefreshToken(AbstractRefreshToken): application = models.ForeignKey( diff --git a/src/tests/api/test_oauth.py b/src/tests/api/test_oauth.py index 082ad6e5e2..71ce92ee7c 100644 --- a/src/tests/api/test_oauth.py +++ b/src/tests/api/test_oauth.py @@ -70,14 +70,27 @@ def admin_user(admin_team): @pytest.fixture -def application(): +def app_developer(): + return User.objects.create_user('app-developer@example.org', 'app-developer') + + +@pytest.fixture +def client2(): + # We need a second test client instance to log in as the app developer user + from django.test import Client + return Client() + + +@pytest.fixture +def application(app_developer): secret = get_random_string(32) a = OAuthApplication.objects.create( name="pretalx", redirect_uris="https://pretalx.com", client_type='confidential', client_secret=secret, - authorization_grant_type='authorization-code' + authorization_grant_type='authorization-code', + user=app_developer, ) a._cached_secret = secret a.save() @@ -703,6 +716,47 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O assert list(grant.organizers.all()) == [organizer] +@pytest.mark.django_db +def test_token_app_disabled(client, client2, admin_user, organizer, application: OAuthApplication, app_developer): + client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( + application.client_id, quote(application.redirect_uris) + )) + assert resp.status_code == 200 + resp = client.post('/api/v1/oauth/authorize', data={ + 'organizers': str(organizer.pk), + 'redirect_uri': application.redirect_uris, + 'scope': 'read write', + 'client_id': application.client_id, + 'response_type': 'code', + 'allow': 'Authorize', + }) + assert resp.status_code == 302 + assert resp['Location'].startswith('https://pretalx.com?code=') + code = resp['Location'].split("=")[1] + client.logout() + resp = client.post('/api/v1/oauth/token', data={ + 'code': code, + 'redirect_uri': application.redirect_uris, + 'grant_type': 'authorization_code', + }, HTTP_AUTHORIZATION='Basic ' + base64.b64encode( + ('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode()) + assert resp.status_code == 200 + data = json.loads(resp.content.decode()) + access_token = data['access_token'] + resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token) + assert resp.status_code == 200 + + client2.login(email='app-developer@example.org', password='app-developer') + client2.post(f'/control/settings/oauth/apps/{application.pk}/disable', {}) + + resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token) + assert resp.status_code == 401 + + @pytest.mark.django_db def test_user_revoke(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy')