Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
8655486a58 Release 1.9.1 2017-11-25 19:08:09 +01:00
Raphael Michel
9d410f33a0 [SECURITY] Fix handling of session timeouts 2017-11-25 19:07:58 +01:00
Raphael Michel
c236144c68 [SECURITY] Respect session timeout in API 2017-11-25 19:07:56 +01:00
6 changed files with 152 additions and 4 deletions

View File

@@ -1 +1 @@
__version__ = "1.9.0"
__version__ = "1.9.1"

View File

@@ -1,3 +1,7 @@
import time
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission
@@ -21,6 +25,18 @@ class EventPermission(BasePermission):
else:
required_permission = None
if request.user.is_authenticated:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return False
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return False
request.session['pretix_auth_last_used'] = int(time.time())
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:

View File

@@ -64,15 +64,17 @@ class PermissionMiddleware(MiddlewareMixin):
return self._login_redirect(request)
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
# If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return self._login_redirect(request)
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE and url_name != 'user.reauth':
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if url_name != 'user.reauth':
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
request.session['pretix_auth_last_used'] = int(time.time())
request.session['pretix_auth_last_used'] = int(time.time())
if 'event' in url.kwargs and 'organizer' in url.kwargs:
request.event = Event.objects.filter(

View File

@@ -50,6 +50,17 @@ def user():
return User.objects.create_user('dummy@dummy.dummy', 'dummy')
@pytest.fixture
def user_client(client, team, user):
team.can_view_orders = True
team.can_view_vouchers = True
team.all_events = True
team.save()
team.members.add(user)
client.force_authenticate(user=user)
return client
@pytest.fixture
def token_client(client, team):
team.can_view_orders = True

View File

@@ -1,4 +1,7 @@
import time
import pytest
from django.test import override_settings
from pretix.base.models import Organizer
@@ -130,3 +133,99 @@ def test_token_event_permission_not_allowed(token_client, team, organizer, event
assert resp.status_code == 403
else:
assert resp.status_code in (404, 403)
@pytest.mark.django_db
def test_log_out_after_absolute_timeout(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 403
@pytest.mark.django_db
def test_dont_logout_before_absolute_timeout(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 + 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 200
@pytest.mark.django_db
@override_settings(PRETIX_LONG_SESSIONS=False)
def test_ignore_long_session_if_disabled_in_config(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 403
@pytest.mark.django_db
def test_dont_logout_in_long_session(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 200
@pytest.mark.django_db
def test_log_out_after_relative_timeout(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 403
@pytest.mark.django_db
def test_dont_logout_before_relative_timeout(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 + 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 200
@pytest.mark.django_db
def test_dont_logout_by_relative_in_long_session(user_client, team, organizer, event):
session = user_client.session
session['pretix_auth_long_session'] = True
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 200
@pytest.mark.django_db
def test_update_session_activity(user_client, team, organizer, event):
t1 = int(time.time()) - 5
session = user_client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
session['pretix_auth_last_used'] = t1
session.save()
response = user_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert response.status_code == 200
assert user_client.session['pretix_auth_last_used'] > t1

View File

@@ -566,6 +566,26 @@ class SessionTimeOutTest(TestCase):
response = self.client.get('/control/')
self.assertEqual(response.status_code, 200)
def test_log_out_after_relative_timeout_really_enforced(self):
# Regression test added after a security problem in 1.9.1
# The problem was that, once the relative timeout happened, the user was redirected
# to /control/reauth/, but loading /control/reauth/ was already considered to be
# "session activitiy". Therefore, after loding /control/reauth/, the session was no longer
# in the timeout state and the user was able to access pages again without re-entering the
# password.
session = self.client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
session.save()
response = self.client.get('/control/')
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, '/control/reauth/?next=/control/')
self.client.get('/control/reauth/?next=/control/')
response = self.client.get('/control/')
self.assertEqual(response.status_code, 302)
def test_update_session_activity(self):
t1 = int(time.time()) - 5
session = self.client.session