diff --git a/src/pretix/base/templatetags/rich_text.py b/src/pretix/base/templatetags/rich_text.py
index 307a2d6d5..3a88bc3c4 100644
--- a/src/pretix/base/templatetags/rich_text.py
+++ b/src/pretix/base/templatetags/rich_text.py
@@ -54,6 +54,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 +159,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 687338f4a..5a282fc3a 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 5b9b6592d..7cfd62f6e 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(