diff --git a/src/pretix/api/auth/permission.py b/src/pretix/api/auth/permission.py index f5887d2534..77c5421aca 100644 --- a/src/pretix/api/auth/permission.py +++ b/src/pretix/api/auth/permission.py @@ -1,12 +1,11 @@ -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 from pretix.base.models import Event from pretix.base.models.organizer import Organizer, TeamAPIToken +from pretix.helpers.security import ( + SessionInvalid, SessionReauthRequired, assert_session_valid, +) class EventPermission(BasePermission): @@ -24,16 +23,13 @@ class EventPermission(BasePermission): 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()) + try: + # If this logic is updated, make sure to also update the logic in pretix/control/middleware.py + assert_session_valid(request) + except SessionInvalid: + return False + except SessionReauthRequired: + return False perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user) diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 90da3cb955..bf4253db53 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -1,4 +1,3 @@ -import time from urllib.parse import quote, urljoin, urlparse from django.conf import settings @@ -11,6 +10,9 @@ from django.utils.encoding import force_str from django.utils.translation import ugettext as _ from pretix.base.models import Event, Organizer +from pretix.helpers.security import ( + SessionInvalid, SessionReauthRequired, assert_session_valid, +) class PermissionMiddleware(MiddlewareMixin): @@ -64,18 +66,15 @@ class PermissionMiddleware(MiddlewareMixin): if not request.user.is_authenticated: return self._login_redirect(request) - if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False): + try: # 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) + assert_session_valid(request) + except SessionInvalid: + logout(request) + return self._login_redirect(request) + except SessionReauthRequired: 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()) + return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) if 'event' in url.kwargs and 'organizer' in url.kwargs: request.event = Event.objects.filter( diff --git a/src/pretix/helpers/security.py b/src/pretix/helpers/security.py new file mode 100644 index 0000000000..6e319804e6 --- /dev/null +++ b/src/pretix/helpers/security.py @@ -0,0 +1,37 @@ +import hashlib +import time + +from django.conf import settings + + +class SessionInvalid(Exception): + pass + + +class SessionReauthRequired(Exception): + pass + + +def get_user_agent_hash(request): + return hashlib.sha256(request.META['HTTP_USER_AGENT'].encode()).hexdigest() + + +def assert_session_valid(request): + 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: + request.session['pretix_auth_login_time'] = 0 + raise SessionInvalid() + if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE: + raise SessionReauthRequired() + + if 'HTTP_USER_AGENT' in request.META: + if 'pinned_user_agent' in request.session: + if request.session.get('pinned_user_agent') != get_user_agent_hash(request): + raise SessionInvalid() + else: + request.session['pinned_user_agent'] = get_user_agent_hash(request) + + request.session['pretix_auth_last_used'] = int(time.time()) + return True diff --git a/src/tests/control/test_auth.py b/src/tests/control/test_auth.py index 1f86d145b8..ed2e0eb1b7 100644 --- a/src/tests/control/test_auth.py +++ b/src/tests/control/test_auth.py @@ -598,3 +598,12 @@ class SessionTimeOutTest(TestCase): self.assertEqual(response.status_code, 200) assert self.client.session['pretix_auth_last_used'] > t1 + + def test_pinned_user_agent(self): + self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36' + response = self.client.get('/control/') + self.assertEqual(response.status_code, 200) + + self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) Something else' + response = self.client.get('/control/') + self.assertEqual(response.status_code, 302)