From cf87fa10390cfea9d08b46e7dc22eeffb1cff249 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Sat, 27 Jun 2026 14:05:53 +0200 Subject: [PATCH] [SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602) --------- Co-authored-by: Raphael Michel --- src/pretix/base/templatetags/rich_text.py | 6 ++---- src/pretix/base/views/redirect.py | 18 ++++++++++++++---- src/tests/base/test_rich_text.py | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/pretix/base/templatetags/rich_text.py b/src/pretix/base/templatetags/rich_text.py index 307a2d6d5a..de51ccc47f 100644 --- a/src/pretix/base/templatetags/rich_text.py +++ b/src/pretix/base/templatetags/rich_text.py @@ -42,8 +42,6 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim from bleach.linkifier import build_email_re from django import template from django.conf import settings -from django.core import signing -from django.urls import reverse from django.utils.functional import SimpleLazyObject from django.utils.html import escape from django.utils.http import url_has_allowed_host_and_scheme @@ -54,6 +52,7 @@ from markdown.postprocessors import Postprocessor from markdown.treeprocessors import UnescapeTreeprocessor from tlds import tld_set +from pretix.base.views.redirect import safelink from pretix.helpers.format import SafeFormatter, format_map register = template.Library() @@ -158,8 +157,7 @@ def safelink_callback(attrs, new=False): """ url = html.unescape(attrs.get((None, 'href'), '/')) if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): - signer = signing.Signer(salt='safe-redirect') - attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) + attrs[None, 'href'] = safelink(url) attrs[None, 'target'] = '_blank' attrs[None, 'rel'] = 'noopener' return attrs diff --git a/src/pretix/base/views/redirect.py b/src/pretix/base/views/redirect.py index 687338f4a8..5a282fc3a6 100644 --- a/src/pretix/base/views/redirect.py +++ b/src/pretix/base/views/redirect.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import logging import urllib.parse from django.core import signing @@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse +logger = logging.getLogger(__name__) + def _is_samesite_referer(request): referer = request.headers.get('referer') @@ -42,11 +45,14 @@ def _is_samesite_referer(request): def redir_view(request): - signer = signing.Signer(salt='safe-redirect') try: - url = signer.unsign(request.GET.get('url', '')) + url = signing.Signer(salt='safelink-url').unsign(request.GET.get('url', '')) except signing.BadSignature: - return HttpResponseBadRequest('Invalid parameter') + try: + # Backwards-compatibility for a change in 2026-06, remove after a while + url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', '')) + except signing.BadSignature: + return HttpResponseBadRequest('Invalid parameter') if not _is_samesite_referer(request): u = urllib.parse.urlparse(url) @@ -61,5 +67,9 @@ def redir_view(request): def safelink(url): - signer = signing.Signer(salt='safe-redirect') + url = str(url) + if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")): + logger.warning('Invalid URL passed to safelink: %r', url) + return '#invalid-url' + signer = signing.Signer(salt='safelink-url') return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) diff --git a/src/tests/base/test_rich_text.py b/src/tests/base/test_rich_text.py index 5b9b6592d1..7cfd62f6ef 100644 --- a/src/tests/base/test_rich_text.py +++ b/src/tests/base/test_rich_text.py @@ -119,7 +119,7 @@ def test_linkify_abs(link): assert markdown_compile_email(input) == f"

{output}

" -signer = signing.Signer(salt='safe-redirect') +signer = signing.Signer(salt='safelink-url') @pytest.mark.parametrize(