[SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602)

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
This commit is contained in:
Mira Weller
2026-07-01 14:02:48 +02:00
committed by Raphael Michel
co-authored by Raphael Michel
parent 7ed69a8ef7
commit cf87fa1039
3 changed files with 17 additions and 9 deletions
+2 -4
View File
@@ -42,8 +42,6 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re from bleach.linkifier import build_email_re
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme 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 markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
register = template.Library() register = template.Library()
@@ -158,8 +157,7 @@ def safelink_callback(attrs, new=False):
""" """
url = html.unescape(attrs.get((None, 'href'), '/')) 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:'): 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'] = safelink(url)
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank' attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener' attrs[None, 'rel'] = 'noopener'
return attrs return attrs
+14 -4
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging
import urllib.parse import urllib.parse
from django.core import signing from django.core import signing
@@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request): def _is_samesite_referer(request):
referer = request.headers.get('referer') referer = request.headers.get('referer')
@@ -42,11 +45,14 @@ def _is_samesite_referer(request):
def redir_view(request): def redir_view(request):
signer = signing.Signer(salt='safe-redirect')
try: try:
url = signer.unsign(request.GET.get('url', '')) url = signing.Signer(salt='safelink-url').unsign(request.GET.get('url', ''))
except signing.BadSignature: 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): if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url) u = urllib.parse.urlparse(url)
@@ -61,5 +67,9 @@ def redir_view(request):
def safelink(url): 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)) return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
+1 -1
View File
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
assert markdown_compile_email(input) == f"<p>{output}</p>" assert markdown_compile_email(input) == f"<p>{output}</p>"
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safelink-url')
@pytest.mark.parametrize( @pytest.mark.parametrize(