From f3f42a8a4259d120053dfece4e2150b75a9dd634 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 2 Apr 2024 11:34:02 +0200 Subject: [PATCH] Login: Add logging for incorrect JS hostnames --- src/pretix/base/forms/auth.py | 1 + src/pretix/control/middleware.py | 1 + .../templates/pretixcontrol/auth/base.html | 1 + .../templates/pretixcontrol/auth/login.html | 2 + src/pretix/control/urls.py | 1 + src/pretix/control/views/auth.py | 54 +++++++++++++++++-- src/pretix/static/pretixcontrol/js/auth.js | 21 ++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/pretix/static/pretixcontrol/js/auth.js diff --git a/src/pretix/base/forms/auth.py b/src/pretix/base/forms/auth.py index fc57596c7..1681e4d14 100644 --- a/src/pretix/base/forms/auth.py +++ b/src/pretix/base/forms/auth.py @@ -59,6 +59,7 @@ class LoginForm(forms.Form): username/password logins. """ keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False) + origin = forms.CharField(widget=forms.HiddenInput, required=False) error_messages = { 'invalid_login': _("This combination of credentials is not known to our system."), diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index d40a27f3a..7391def2b 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -67,6 +67,7 @@ class PermissionMiddleware: "auth.forgot.recover", "auth.invite", "user.settings.notifications.off", + "auth.bad_origin_report", ) EXCEPTIONS_FORCED_PW_CHANGE = ( diff --git a/src/pretix/control/templates/pretixcontrol/auth/base.html b/src/pretix/control/templates/pretixcontrol/auth/base.html index bcf23c5fa..6956d60e6 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/base.html +++ b/src/pretix/control/templates/pretixcontrol/auth/base.html @@ -58,5 +58,6 @@ {{ poweredby }} {# removing or hiding this might be in violation of pretix' license #} + diff --git a/src/pretix/control/templates/pretixcontrol/auth/login.html b/src/pretix/control/templates/pretixcontrol/auth/login.html index f343aad72..ff9b9b3bb 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/login.html +++ b/src/pretix/control/templates/pretixcontrol/auth/login.html @@ -46,5 +46,7 @@ {% endif %} + + {# marker required for ajax calls to detect that user session is over #} {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index c9a761b22..230a299f7 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -45,6 +45,7 @@ from pretix.control.views import ( urlpatterns = [ re_path(r'^logout$', auth.logout, name='auth.logout'), re_path(r'^login$', auth.login, name='auth.login'), + re_path(r'^login/bad_origin$', auth.bad_origin_report, name='auth.bad_origin_report'), re_path(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'), re_path(r'^register$', auth.register, name='auth.register'), re_path(r'^invite/(?P[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'), diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index 01ed7e039..574f91c8f 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -36,7 +36,7 @@ import base64 import json import logging import time -from urllib.parse import quote, urlparse +from urllib.parse import quote, urljoin, urlparse import webauthn from django.conf import settings @@ -47,11 +47,14 @@ from django.contrib.auth import ( from django.contrib.auth.tokens import default_token_generator from django.core.exceptions import PermissionDenied from django.db import transaction +from django.http import HttpResponse from django.shortcuts import redirect, render from django.urls import reverse from django.utils.functional import cached_property from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods from django.views.generic import TemplateView from django_otp import match_token from webauthn.helpers import generate_challenge @@ -63,7 +66,7 @@ from pretix.base.forms.auth import ( from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.services.mail import SendMailException -from pretix.helpers.http import redirect_to_url +from pretix.helpers.http import get_client_ip, redirect_to_url logger = logging.getLogger(__name__) @@ -109,6 +112,9 @@ def login(request): return process_login(request, u, False) b.url = b.authentication_url(request) + # Login should only happen on configured main domain + good_origin = urlparse(settings.SITE_URL).scheme + '://' + urlparse(settings.SITE_URL).hostname + backend = backenddict.get(request.GET.get('backend', 'native'), backends[0]) if not backend.visible: backend = [b for b in backends if b.visible][0] @@ -119,7 +125,23 @@ def login(request): return redirect(reverse('control:index')) if request.method == 'POST': form = LoginForm(backend=backend, data=request.POST, request=request) - if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier: + is_valid = form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier + + if form.cleaned_data.get("origin"): + form_origin = form.cleaned_data.get("origin") + if good_origin != form_origin: + logger.warning( + f"Received login form submission with unexpected origin value. " + f"Origin sent from JavaScript: {form_origin} / " + f"Expected origin from configuration: {good_origin} / " + f"HTTP Host header: {request.headers.get('Host')} / " + f"HTTP origin header: {request.headers.get('Origin')} / " + f"HTTP referer header: {request.headers.get('Referer')} / " + f"IP address: {get_client_ip(request)} / " + f"Login result: {is_valid}" + ) + + if is_valid: return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False)) else: form = LoginForm(backend=backend, request=request) @@ -128,9 +150,35 @@ def login(request): ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET ctx['backends'] = backends ctx['backend'] = backend + ctx['good_origin'] = good_origin[::-1] # minimal obfuscation against standard link rewriting + ctx['bad_origin_report_url'] = urljoin( + # as an additional safeguard always use SITE_URL, not anything derived from request + settings.SITE_URL, + reverse('control:auth.bad_origin_report') + )[::-1] return render(request, 'pretixcontrol/auth/login.html', ctx) +@csrf_exempt +@require_http_methods(["POST"]) +def bad_origin_report(request): + good_origin = urlparse(settings.SITE_URL).scheme + '://' + urlparse(settings.SITE_URL).hostname + form_origin = request.POST.get("origin") + if good_origin != form_origin: + logger.warning( + f"Received report of unexpected origin value. " + f"Origin sent from JavaScript: {form_origin} / " + f"Expected origin from configuration: {good_origin} / " + f"HTTP Host header: {request.headers.get('Host')} / " + f"HTTP origin header: {request.headers.get('Origin')} / " + f"HTTP referer header: {request.headers.get('Referer')} / " + f"IP address: {get_client_ip(request)}" + ) + resp = HttpResponse() + resp['Access-Control-Allow-Origin'] = '*' + return resp + + def logout(request): """ Log the user out of the current session, then redirect to login page. diff --git a/src/pretix/static/pretixcontrol/js/auth.js b/src/pretix/static/pretixcontrol/js/auth.js new file mode 100644 index 000000000..f0d3439a2 --- /dev/null +++ b/src/pretix/static/pretixcontrol/js/auth.js @@ -0,0 +1,21 @@ +var hiddenfield = document.querySelector("input[name=origin][type=hidden]"); +if (hiddenfield) { + hiddenfield.value = window.location.origin +} +async function runCheck() { + if (document.getElementById("good_origin")) { + if (document.getElementById("good_origin").innerText.split('').reverse().join('') !== window.location.origin) { + const response = await fetch(document.getElementById("bad_origin_report_url").innerText.split('').reverse().join(''), { + method: "POST", + mode: "cors", + referrerPolicy: "unsafe-url", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "origin=" + window.location.origin, + }); + } + } +} + +runCheck(); \ No newline at end of file