VYPR
Moderate severityNVD Advisory· Published Mar 6, 2022· Updated Aug 2, 2024

Multiple Open Redirect in nitely/spirit

CVE-2022-0869

Description

Spirit prior to 0.12.3 contains multiple open redirect vulnerabilities in comment views, allowing an attacker to redirect users to arbitrary external sites.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Spirit prior to 0.12.3 contains multiple open redirect vulnerabilities in comment views, allowing an attacker to redirect users to arbitrary external sites.

Vulnerability

Multiple open redirect vulnerabilities exist in Spirit versions prior to 0.12.3, a Python/Django forum application [3]. The flaws reside in the publish, update, delete, and move comment views, where user-supplied next parameters from POST or GET requests were used directly in redirect() calls without proper validation [2]. This allowed an attacker to supply a malicious URL as the next parameter, causing the application to redirect the user to an arbitrary external site.

Exploitation

An attacker can exploit this by crafting a link that directs a logged-in user to a legitimate Spirit page (e.g., a comment form) while embedding a malicious URL in the next parameter. Upon submitting a form or performing an action, the application would redirect the user to the attacker-controlled site. No authentication beyond a valid user session is required, and the attack can be triggered via cross-site request forgery (CSRF) if the user is tricked into clicking a crafted link or submitting a form [2]. The fix introduced the safe_redirect helper to validate redirect targets [2].

Impact

A successful attack can redirect users to malicious websites, enabling phishing attacks, credential theft, or distribution of malware. The open redirect can also be used to bypass URL validation schemes and harm the application's reputation. While no direct data exposure or code execution occurs, the open redirect facilitates social engineering and trust exploitation.

Mitigation

The vulnerability is fixed in Spirit version 0.12.3, released on or around February 26, 2022 [1]. Users should upgrade to this version or later. The commit [2] replaced insecure redirect() calls with safe_redirect() from the Spirit core, ensuring only safe redirects are allowed. If upgrading is not immediately possible, site administrators should monitor for suspicious next parameter usage and consider implementing network-level controls to restrict outgoing redirects, though the vendor patch is the recommended solution.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
django-spiritPyPI
< 0.12.30.12.3

Affected products

2

Patches

1
8f32f89654d6

fix unsafe redirect (#308)

https://github.com/nitely/spiritEsteban C BorsaniFeb 23, 2022via ghsa
15 files changed · +111 64
  • spirit/admin/views.py+3 2 modified
    @@ -1,12 +1,13 @@
     # -*- coding: utf-8 -*-
     
    -from django.shortcuts import render, redirect
    +from django.shortcuts import render
     from django.contrib import messages
     from django.utils.translation import gettext as _
     from django.contrib.auth import get_user_model
     
     import spirit
     import django
    +from spirit.core.utils.http import safe_redirect
     from spirit.category.models import Category
     from spirit.comment.flag.models import CommentFlag
     from spirit.comment.like.models import CommentLike
    @@ -25,7 +26,7 @@ def config_basic(request):
         if is_post(request) and form.is_valid():
             form.save()
             messages.info(request, _("Settings updated!"))
    -        return redirect(request.GET.get("next", request.get_full_path()))
    +        return safe_redirect(request, "next", request.get_full_path())
         return render(
             request=request,
             template_name='spirit/admin/config_basic.html',
    
  • spirit/comment/flag/views.py+4 3 modified
    @@ -1,9 +1,10 @@
     # -*- coding: utf-8 -*-
     
     from django.contrib.auth.decorators import login_required
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     
    -from ...core.utils.views import is_post, post_data
    +from spirit.core.utils.http import safe_redirect
    +from spirit.core.utils.views import is_post, post_data
     from ..models import Comment
     from .forms import FlagForm
     
    @@ -18,7 +19,7 @@ def create(request, comment_id):
     
         if is_post(request) and form.is_valid():
             form.save()
    -        return redirect(request.POST.get('next', comment.get_absolute_url()))
    +        return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    
  • spirit/comment/like/views.py+5 3 modified
    @@ -1,9 +1,10 @@
     # -*- coding: utf-8 -*-
     
     from django.contrib.auth.decorators import login_required
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     from django.urls import reverse
     
    +from spirit.core.utils.http import safe_redirect
     from spirit.core.utils.views import is_post, post_data, is_ajax
     from spirit.core.utils import json_response
     from spirit.comment.models import Comment
    @@ -28,7 +29,7 @@ def create(request, comment_id):
             if is_ajax(request):
                 return json_response({'url_delete': like.get_delete_url()})
     
    -        return redirect(request.POST.get('next', comment.get_absolute_url()))
    +        return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    @@ -52,7 +53,8 @@ def delete(request, pk):
                     kwargs={'comment_id': like.comment.pk})
                 return json_response({'url_create': url, })
     
    -        return redirect(request.POST.get('next', like.comment.get_absolute_url()))
    +        return safe_redirect(
    +            request, 'next', like.comment.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    
  • spirit/comment/poll/views.py+7 6 modified
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
     from django.contrib.auth.decorators import login_required
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     from django.views.decorators.http import require_POST
     from django.contrib import messages
     from django.contrib.auth.views import redirect_to_login
    @@ -10,8 +10,9 @@
     
     from djconfig import config
     
    -from ...core import utils
    -from ...core.utils.paginator import yt_paginate
    +from spirit.core.utils.http import safe_redirect
    +from spirit.core import utils
    +from spirit.core.utils.paginator import yt_paginate
     from .models import CommentPoll, CommentPollChoice, CommentPollVote
     from .forms import PollVoteManyForm
     
    @@ -35,7 +36,7 @@ def close_or_open(request, pk, close=True):
          .filter(pk=poll.pk)
          .update(close_at=close_at))
     
    -    return redirect(request.GET.get('next', poll.get_absolute_url()))
    +    return safe_redirect(request, 'next', poll.get_absolute_url())
     
     
     @require_POST
    @@ -55,10 +56,10 @@ def vote(request, pk):
             CommentPollChoice.decrease_vote_count(poll=poll, voter=request.user)
             form.save_m2m()
             CommentPollChoice.increase_vote_count(poll=poll, voter=request.user)
    -        return redirect(request.POST.get('next', poll.get_absolute_url()))
    +        return safe_redirect(request, 'next', poll.get_absolute_url(), method='POST')
     
         messages.error(request, utils.render_form_errors(form))
    -    return redirect(request.POST.get('next', poll.get_absolute_url()))
    +    return safe_redirect(request, 'next', poll.get_absolute_url(), method='POST')
     
     
     @login_required
    
  • spirit/comment/views.py+7 7 modified
    @@ -8,6 +8,7 @@
     
     from djconfig import config
     
    +from spirit.core.utils.http import safe_redirect
     from spirit.core.utils.views import is_post, post_data, is_ajax
     from spirit.core.utils.ratelimit.decorators import ratelimit
     from spirit.core.utils.decorators import moderator_required
    @@ -41,15 +42,14 @@ def publish(request, topic_id, pk=None):
         if is_post(request) and not request.is_limited() and form.is_valid():
             if not user.st.update_post_hash(form.get_comment_hash()):
                 # Hashed comment may have not been saved yet
    -            return redirect(
    -                request.POST.get('next', None) or
    -                Comment
    +            default_url = lambda: (Comment
                     .get_last_for_topic(topic_id)
                     .get_absolute_url())
    +            return safe_redirect(request, 'next', default_url, method='POST')
     
             comment = form.save()
             comment_posted(comment=comment, mentions=form.mentions)
    -        return redirect(request.POST.get('next', comment.get_absolute_url()))
    +        return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    @@ -67,7 +67,7 @@ def update(request, pk):
             pre_comment_update(comment=Comment.objects.get(pk=comment.pk))
             comment = form.save()
             post_comment_update(comment=comment)
    -        return redirect(request.POST.get('next', comment.get_absolute_url()))
    +        return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
         return render(
             request=request,
             template_name='spirit/comment/update.html',
    @@ -81,7 +81,7 @@ def delete(request, pk, remove=True):
             (Comment.objects
              .filter(pk=pk)
              .update(is_removed=remove))
    -        return redirect(request.GET.get('next', comment.get_absolute_url()))
    +        return safe_redirect(request, 'next', comment.get_absolute_url())
         return render(
             request=request,
             template_name='spirit/comment/moderate.html',
    @@ -104,7 +104,7 @@ def move(request, topic_id):
         else:
             messages.error(request, render_form_errors(form))
     
    -    return redirect(request.POST.get('next', topic.get_absolute_url()))
    +    return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
     
     
     def find(request, pk):
    
  • spirit/core/utils/decorators.py+2 2 modified
    @@ -4,9 +4,9 @@
     
     from django.core.exceptions import PermissionDenied
     from django.contrib.auth.views import redirect_to_login
    -from django.shortcuts import redirect
     
     from spirit.core.conf import settings
    +from spirit.core.utils.http import safe_redirect
     
     
     def moderator_required(view_func):
    @@ -48,7 +48,7 @@ def guest_only(view_func):
         @wraps(view_func)
         def wrapper(request, *args, **kwargs):
             if request.user.is_authenticated:
    -            return redirect(request.GET.get('next', request.user.st.get_absolute_url()))
    +            return safe_redirect(request, 'next', request.user.st.get_absolute_url())
     
             return view_func(request, *args, **kwargs)
     
    
  • spirit/core/utils/http.py+29 0 added
    @@ -0,0 +1,29 @@
    +# -*- coding: utf-8 -*-
    +
    +from django.shortcuts import redirect
    +from django.utils.encoding import iri_to_uri
    +
    +try:
    +    from django.utils.http import url_has_allowed_host_and_scheme
    +except ImportError:
    +    from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme
    +
    +
    +def _resolve_lazy_url(url):
    +    if callable(url):
    +        return url()
    +    return url
    +
    +
    +def safe_redirect(request, key, default_url='', method='GET'):
    +    next = (
    +        getattr(request, method).get(key, None) or
    +        _resolve_lazy_url(default_url)
    +    )
    +    url_is_safe = url_has_allowed_host_and_scheme(
    +        url=next, allowed_hosts=None)
    +        #allowed_hosts=settings.ALLOWED_HOSTS,
    +        #require_https=request.is_secure())
    +    if url_is_safe:
    +        return redirect(iri_to_uri(next))
    +    return redirect('/')
    
  • spirit/topic/favorite/views.py+4 4 modified
    @@ -2,14 +2,14 @@
     
     from django.contrib.auth.decorators import login_required
     from django.shortcuts import get_object_or_404
    -from django.shortcuts import redirect
     from django.views.decorators.http import require_POST
     from django.contrib import messages
     
     from .models import TopicFavorite
     from .forms import FavoriteForm
     from ..models import Topic
    -from ...core import utils
    +from spirit.core import utils
    +from spirit.core.utils.http import safe_redirect
     
     
     @require_POST
    @@ -23,12 +23,12 @@ def create(request, topic_id):
         else:
             messages.error(request, utils.render_form_errors(form))
     
    -    return redirect(request.POST.get('next', topic.get_absolute_url()))
    +    return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
     
     
     @require_POST
     @login_required
     def delete(request, pk):
         favorite = get_object_or_404(TopicFavorite, pk=pk, user=request.user)
         favorite.delete()
    -    return redirect(request.POST.get('next', favorite.topic.get_absolute_url()))
    +    return safe_redirect(request, 'next', favorite.topic.get_absolute_url(), method='POST')
    
  • spirit/topic/moderate/views.py+3 3 modified
    @@ -1,10 +1,11 @@
     # -*- coding: utf-8 -*-
     
     from django.utils import timezone
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     from django.contrib import messages
     from django.utils.translation import gettext as _
     
    +from spirit.core.utils.http import safe_redirect
     from spirit.core.utils.views import is_post
     from spirit.core.utils.decorators import moderator_required
     from spirit.comment.models import Comment
    @@ -33,8 +34,7 @@ def _moderate(request, pk, field_name, to_value, action=None, message=None):
             if message is not None:
                 messages.info(request, message)
     
    -        return redirect(request.POST.get(
    -            'next', topic.get_absolute_url()))
    +        return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    
  • spirit/topic/notification/views.py+7 6 modified
    @@ -3,7 +3,7 @@
     import json
     
     from django.contrib.auth.decorators import login_required
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     from django.views.decorators.http import require_POST
     from django.http import Http404, HttpResponse
     from django.contrib import messages
    @@ -18,6 +18,7 @@
     from spirit.core.utils.paginator import yt_paginate
     from spirit.core.utils.paginator.infinite_paginator import paginate
     from spirit.core.utils.views import is_ajax
    +from spirit.core.utils.http import safe_redirect
     from spirit.topic.models import Topic
     from .models import TopicNotification
     from .forms import NotificationForm, NotificationCreationForm
    @@ -39,7 +40,7 @@ def create(request, topic_id):
         else:
             messages.error(request, utils.render_form_errors(form))
     
    -    return redirect(request.POST.get('next', topic.get_absolute_url()))
    +    return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
     
     
     @require_POST
    @@ -53,8 +54,8 @@ def update(request, pk):
         else:
             messages.error(request, utils.render_form_errors(form))
     
    -    return redirect(request.POST.get(
    -        'next', notification.topic.get_absolute_url()))
    +    return safe_redirect(
    +        request, 'next', notification.topic.get_absolute_url(), method='POST')
     
     
     @login_required
    @@ -124,5 +125,5 @@ def mark_all_as_read(request):
             .for_access(request.user)
             .filter(is_read=False)
             .update(is_read=True))
    -    return redirect(request.POST.get(
    -        'next', reverse('spirit:topic:notification:index')))
    +    return safe_redirect(
    +        request, 'next', reverse('spirit:topic:notification:index'), method='POST')
    
  • spirit/topic/private/views.py+17 14 modified
    @@ -10,14 +10,15 @@
     
     from djconfig import config
     
    -from ...core.conf import settings
    -from ...core import utils
    -from ...core.utils.views import is_post, post_data
    -from ...core.utils.paginator import paginate, yt_paginate
    -from ...core.utils.ratelimit.decorators import ratelimit
    -from ...comment.forms import CommentForm
    -from ...comment.utils import comment_posted
    -from ...comment.models import Comment
    +from spirit.core.conf import settings
    +from spirit.core import utils
    +from spirit.core.utils.http import safe_redirect
    +from spirit.core.utils.views import is_post, post_data
    +from spirit.core.utils.paginator import paginate, yt_paginate
    +from spirit.core.utils.ratelimit.decorators import ratelimit
    +from spirit.comment.forms import CommentForm
    +from spirit.comment.utils import comment_posted
    +from spirit.comment.models import Comment
     from ..models import Topic
     from ..utils import topic_viewed
     from .utils import notify_access
    @@ -50,9 +51,8 @@ def publish(request, user_id=None):
                 all([tform.is_valid(), cform.is_valid(), tpform.is_valid()]) and
                 not request.is_limited()):
             if not user.st.update_post_hash(tform.get_topic_hash()):
    -            return redirect(
    -                request.POST.get('next', None) or
    -                tform.category.get_absolute_url())
    +            return safe_redirect(
    +                request, 'next', lambda: tform.category.get_absolute_url(), method='POST')
     
             # wrap in transaction.atomic?
             topic = tform.save()
    @@ -123,7 +123,8 @@ def create_access(request, topic_id):
         else:
             messages.error(request, utils.render_form_errors(form))
     
    -    return redirect(request.POST.get('next', topic_private.get_absolute_url()))
    +    return safe_redirect(
    +        request, 'next', topic_private.get_absolute_url(), method='POST')
     
     
     @login_required
    @@ -136,7 +137,8 @@ def delete_access(request, pk):
             if request.user.pk == topic_private.user_id:
                 return redirect(reverse("spirit:topic:private:index"))
     
    -        return redirect(request.POST.get('next', topic_private.get_absolute_url()))
    +        return safe_redirect(
    +            request, 'next', topic_private.get_absolute_url(), method='POST')
     
         return render(
             request=request,
    @@ -160,7 +162,8 @@ def join_in(request, topic_id):
         if is_post(request) and form.is_valid():
             topic_private = form.save()
             notify_access(user=form.get_user(), topic_private=topic_private)
    -        return redirect(request.POST.get('next', topic.get_absolute_url()))
    +        return safe_redirect(
    +            request, 'next', topic.get_absolute_url(), method='POST')
         return render(
             request=request,
             template_name='spirit/topic/private/join.html',
    
  • spirit/topic/views.py+5 4 modified
    @@ -6,6 +6,7 @@
     
     from djconfig import config
     
    +from spirit.core.utils.http import safe_redirect
     from spirit.core.utils.views import is_post, post_data
     from spirit.core.utils.paginator import paginate, yt_paginate
     from spirit.core.utils.ratelimit.decorators import ratelimit
    @@ -38,9 +39,9 @@ def publish(request, category_id=None):
                 all([form.is_valid(), cform.is_valid()]) and
                 not request.is_limited()):
             if not user.st.update_post_hash(form.get_topic_hash()):
    -            return redirect(
    -                request.POST.get('next', None) or
    -                form.get_category().get_absolute_url())
    +            default_url = lambda: form.get_category().get_absolute_url()
    +            return safe_redirect(
    +                request, 'next', default_url, method='POST')
             # wrap in transaction.atomic?
             topic = form.save()
             cform.topic = topic
    @@ -66,7 +67,7 @@ def update(request, pk):
             if topic.category_id != category_id:
                 Comment.create_moderation_action(
                     user=request.user, topic=topic, action=Comment.MOVED)
    -        return redirect(request.POST.get('next', topic.get_absolute_url()))
    +        return safe_redirect(request,'next', topic.get_absolute_url(), method='POST')
         return render(
             request=request,
             template_name='spirit/topic/update.html',
    
  • spirit/user/admin/views.py+6 5 modified
    @@ -1,15 +1,16 @@
     # -*- coding: utf-8 -*-
     
    -from django.shortcuts import render, redirect, get_object_or_404
    +from django.shortcuts import render, get_object_or_404
     from django.contrib.auth import get_user_model
     from django.contrib import messages
     from django.utils.translation import gettext as _
     
     from djconfig import config
     
    -from ...core.utils.views import is_post, post_data
    -from ...core.utils.paginator import yt_paginate
    -from ...core.utils.decorators import administrator_required
    +from spirit.core.utils.http import safe_redirect
    +from spirit.core.utils.views import is_post, post_data
    +from spirit.core.utils.paginator import yt_paginate
    +from spirit.core.utils.decorators import administrator_required
     from .forms import UserForm, UserProfileForm
     
     User = get_user_model()
    @@ -24,7 +25,7 @@ def edit(request, user_id):
             uform.save()
             form.save()
             messages.info(request, _("This profile has been updated!"))
    -        return redirect(request.GET.get("next", request.get_full_path()))
    +        return safe_redirect(request, "next", request.get_full_path())
         return render(
             request=request,
             template_name='spirit/user/admin/edit.html',
    
  • spirit/user/auth/tests/tests.py+5 0 modified
    @@ -55,6 +55,11 @@ def test_login_redirect(self):
             response = self.client.get(reverse('spirit:user:auth:login') + '?next=/fakepath/')
             self.assertRedirects(response, '/fakepath/', status_code=302, target_status_code=404)
     
    +    def test_login_open_redirect(self):
    +        utils.login(self)
    +        response = self.client.get(reverse('spirit:user:auth:login') + '?next=https%3A%2F%2Fevil.com')
    +        self.assertRedirects(response, '/', status_code=302)
    +
         @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
         def test_login_email_case_insensitive(self):
             """
    
  • spirit/user/auth/views.py+7 5 modified
    @@ -9,6 +9,7 @@
     from django.urls import reverse_lazy
     
     from spirit.core.conf import settings
    +from spirit.core.utils.http import safe_redirect
     from spirit.core.utils.views import is_post, post_data
     from spirit.core.utils.ratelimit.decorators import ratelimit
     from spirit.user.utils.email import send_activation_email
    @@ -62,7 +63,8 @@ class _CustomLoginView(django_views.LoginView):
     def custom_login(request, **kwargs):
         # Currently, Django 1.5 login view does not redirect somewhere if the user is logged in
         if request.user.is_authenticated:
    -        return redirect(request.GET.get('next', request.user.st.get_absolute_url()))
    +        return safe_redirect(
    +            request, 'next', request.user.st.get_absolute_url())
     
         if request.method == "POST" and request.is_limited():
             return redirect(request.get_full_path())
    @@ -73,7 +75,7 @@ def custom_login(request, **kwargs):
     # TODO: @login_required ?
     def custom_logout(request, **kwargs):
         if not request.user.is_authenticated:
    -        return redirect(request.GET.get('next', reverse(settings.LOGIN_URL)))
    +        return safe_redirect(request, 'next', reverse(settings.LOGIN_URL))
     
         if request.method == 'POST':
             return _logout_view(request, **kwargs)
    @@ -93,7 +95,7 @@ def custom_password_reset(request, **kwargs):
     # TODO: @guest_only
     def register(request, registration_form=RegistrationForm):
         if request.user.is_authenticated:
    -        return redirect(request.GET.get('next', reverse('spirit:user:update')))
    +        return safe_redirect(request, 'next', reverse('spirit:user:update'))
     
         form = registration_form(data=post_data(request))
         if (is_post(request) and
    @@ -109,7 +111,7 @@ def register(request, registration_form=RegistrationForm):
             # TODO: email-less activation
             # if not settings.REGISTER_EMAIL_ACTIVATION_REQUIRED:
             # login(request, user)
    -        # return redirect(request.GET.get('next', reverse('spirit:user:update')))
    +        # return safe_redirect(request, 'next', reverse('spirit:user:update'))
     
             return redirect(reverse(settings.LOGIN_URL))
         return render(
    @@ -135,7 +137,7 @@ def registration_activation(request, pk, token):
     # TODO: @guest_only
     def resend_activation_email(request):
         if request.user.is_authenticated:
    -        return redirect(request.GET.get('next', reverse('spirit:user:update')))
    +        return safe_redirect(request, 'next', reverse('spirit:user:update'))
     
         form = ResendActivationForm(data=post_data(request))
         if is_post(request):
    

Vulnerability mechanics

Root cause

"The application failed to validate user-supplied redirect URLs, allowing for open redirection."

Attack vector

An attacker can trigger an open redirect by providing a malicious URL in the `next` parameter of a POST or GET request to various endpoints within the `spirit` application. Because the application previously trusted this input without validation, it would redirect the user to an arbitrary external domain [patch_id=17806]. This vulnerability allows attackers to conduct phishing attacks by redirecting users to malicious sites under the guise of the legitimate application.

Affected code

Multiple view functions across the `spirit` application were affected, including those in `spirit/topic/private/views.py`, `spirit/comment/views.py`, `spirit/comment/poll/views.py`, `spirit/topic/notification/views.py`, `spirit/user/auth/views.py`, `spirit/user/admin/views.py`, `spirit/topic/views.py`, `spirit/comment/like/views.py`, and `spirit/topic/favorite/views.py`. These views improperly handled user-supplied redirect URLs by directly passing them to the `redirect()` function [patch_id=17806].

What the fix does

The patch introduces a new utility function `safe_redirect` in `spirit/core/utils/http.py` which utilizes `url_has_allowed_host_and_scheme` to validate the target URL before performing a redirect [patch_id=17806]. All identified vulnerable views were updated to use this `safe_redirect` function instead of the standard Django `redirect` function. This ensures that only safe, local URLs are processed, effectively preventing open redirect attacks [patch_id=17806].

Preconditions

  • inputThe attacker must be able to send a request to the application with a 'next' parameter containing an arbitrary URL.

Generated on May 17, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.