Login: Add logging for incorrect JS hostnames

This commit is contained in:
Raphael Michel
2024-04-02 11:34:02 +02:00
parent 20d0a9a0ed
commit f3f42a8a42
7 changed files with 78 additions and 3 deletions

View File

@@ -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."),

View File

@@ -67,6 +67,7 @@ class PermissionMiddleware:
"auth.forgot.recover",
"auth.invite",
"user.settings.notifications.off",
"auth.bad_origin_report",
)
EXCEPTIONS_FORCED_PW_CHANGE = (

View File

@@ -58,5 +58,6 @@
{{ poweredby }} {# removing or hiding this might be in violation of pretix' license #}
</footer>
</div>
<script type="text/javascript" src="{% static "pretixcontrol/js/auth.js" %}"></script>
</body>
</html>

View File

@@ -46,5 +46,7 @@
{% endif %}
</div>
</form>
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
{% endblock %}

View File

@@ -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<token>[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'),

View File

@@ -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.

View File

@@ -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();