VYPR
Moderate severityNVD Advisory· Published Sep 30, 2021· Updated Apr 30, 2025

Shuup - Reflected XSS in Error Page

CVE-2021-25963

Description

In Shuup, versions 1.6.0 through 2.10.8 are vulnerable to reflected Cross-Site Scripting (XSS) that allows execution of arbitrary javascript code on a victim browser. This vulnerability exists due to the error page contents not escaped.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
shuupPyPI
>= 1.6.0, < 2.11.02.11.0

Affected products

1

Patches

1
75714c37e327

General: fix critical views that can be subject of XSS attacks

https://github.com/shuup/shuupChristian HessJul 6, 2021via ghsa
10 files changed · +28 11
  • CHANGELOG.md+9 0 modified
    @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
     
     List all changes after the last release here (newer on top). Each change on a separate bullet point line
     
    +### Fixed
    +
    +- General: fix critical vulnerability on views that were returning not escaped content making it open to XSS attacks
    +
    +### Changed
    +
    +- Admin: hide email template button based on permission
    +- Reports: improve log when an importer fails
    +
     ## [2.10.8] - 2021-06-30
     
     ### Changed
    
  • shuup/admin/utils/urls.py+2 1 modified
    @@ -16,6 +16,7 @@
     from django.core.exceptions import ImproperlyConfigured
     from django.http.response import HttpResponseForbidden
     from django.utils.encoding import force_str, force_text
    +from django.utils.html import escape
     from django.utils.http import urlencode
     from django.utils.translation import ugettext_lazy as _
     
    @@ -64,7 +65,7 @@ def _get_unauth_response(self, request, reason):
                 # Instead of redirecting to the login page, let the user know what's wrong with
                 # a helpful link.
                 raise (
    -                Problem(_("Can't view this page. %(reason)s") % {"reason": reason}).with_link(
    +                Problem(_("Can't view this page. %(reason)s") % {"reason": escape(reason)}).with_link(
                         url=resp.url, title=_("Log in with different credentials...")
                     )
                 )
    
  • shuup/core/basket/command_dispatcher.py+2 1 modified
    @@ -11,6 +11,7 @@
     from django.core.exceptions import ValidationError
     from django.http import HttpResponseRedirect, JsonResponse
     from django.shortcuts import redirect
    +from django.utils.html import escape
     from django.utils.translation import ugettext_lazy as _
     
     from shuup.apps.provides import get_provide_objects
    @@ -68,7 +69,7 @@ def handle(self, command, kwargs=None):
             try:
                 handler = self.get_command_handler(command)
                 if not handler or not callable(handler):
    -                raise Problem(_("Error! Invalid command `%s`.") % command)
    +                raise Problem(_("Error! Invalid command `%s`.") % escape(command))
                 kwargs.pop("csrfmiddlewaretoken", None)  # The CSRF token should never be passed as a kwarg
                 kwargs.pop("command", None)  # Nor the command
                 kwargs.update(request=self.request, basket=self.basket)
    
  • shuup/front/checkout/_process.py+2 1 modified
    @@ -9,6 +9,7 @@
     from collections import OrderedDict
     from django.core.exceptions import ImproperlyConfigured
     from django.http.response import Http404
    +from django.utils.html import escape
     
     from shuup.front.basket import get_basket
     from shuup.utils.django_compat import reverse
    @@ -75,7 +76,7 @@ def get_current_phase(self, requested_phase_identifier):
                         return phase
                 if not phase.should_skip() and not phase.is_valid():  # A past phase is not valid, that's the current one
                     return phase
    -        raise Http404("Error! Phase with identifier `%s` not found." % requested_phase_identifier)  # pragma: no cover
    +        raise Http404("Error! Phase with identifier `%s` not found." % escape(requested_phase_identifier))
     
         def _get_next_phase(self, phases, current_phase, target_phase):
             found = False
    
  • shuup/front/urls.py+2 1 modified
    @@ -10,6 +10,7 @@
     from django.conf.urls import url
     from django.contrib.auth.decorators import login_required
     from django.http.response import HttpResponse
    +from django.utils.html import escape
     from django.views.decorators.csrf import csrf_exempt
     from django.views.i18n import set_language
     from itertools import chain
    @@ -37,7 +38,7 @@
     
     
     def _not_here_yet(request, *args, **kwargs):
    -    return HttpResponse("Not here yet: %s (%r, %r)" % (request.path, args, kwargs), status=410)
    +    return HttpResponse("Not here yet: %s (%r, %r)" % (request.path, escape(args), escape(kwargs)), status=410)
     
     
     # Use a different js catalog function in front urlpatterns to prevent forcing
    
  • shuup/utils/excs.py+4 3 modified
    @@ -1,4 +1,5 @@
     from django.core.exceptions import ValidationError
    +from django.utils.html import escape
     
     from shuup.utils.django_compat import force_text
     
    @@ -61,10 +62,10 @@ def extract_messages(obj_list):
         for obj in obj_list:
             if isinstance(obj, ValidationError):
                 for msg in obj.messages:
    -                yield force_text(msg)
    +                yield escape(force_text(msg))
                 continue
             if isinstance(obj, Exception):
                 if len(obj.args):
    -                yield force_text(obj.args[0])
    +                yield escape(force_text(obj.args[0]))
                     continue
    -        yield force_text(obj)
    +        yield escape(force_text(obj))
    
  • shuup/xtheme/urls.py+1 1 modified
    @@ -19,7 +19,7 @@
     
     urlpatterns = [
         url(r"^xtheme/editor/$", EditorView.as_view(), name="xtheme_editor"),
    -    url(r"^xtheme/(?P<view>.+)/*$", extra_view_dispatch, name="xtheme_extra_view"),
    +    url(r"^xtheme/(?P<view>.+)/?$", extra_view_dispatch, name="xtheme_extra_view"),
         url(r"^xtheme/$", command_dispatch, name="xtheme"),
         url(
             r"^xtheme-prod-hl/(?P<plugin_type>.*)/(?P<cutoff_days>\d+)/(?P<count>\d+)/(?P<cache_timeout>\d+)/$",
    
  • shuup/xtheme/views/command.py+2 1 modified
    @@ -6,6 +6,7 @@
     # This source code is licensed under the OSL-3.0 license found in the
     # LICENSE file in the root directory of this source tree.
     from django.http.response import HttpResponseRedirect
    +from django.utils.html import escape
     
     from shuup.utils.excs import Problem
     from shuup.xtheme.editing import set_edit_mode
    @@ -42,4 +43,4 @@ def command_dispatch(request):
             response = handle_command(request, command)
             if response:
                 return response
    -    raise Problem("Error! Unknown command: `%r`" % command)
    +    raise Problem("Error! Unknown command: `%r`" % escape(command))
    
  • shuup/xtheme/views/editor.py+2 1 modified
    @@ -8,6 +8,7 @@
     import json
     from django.http.response import HttpResponse, HttpResponseRedirect
     from django.middleware.csrf import get_token
    +from django.utils.html import escape
     from django.utils.http import urlencode
     from django.utils.translation import ugettext_lazy as _
     from django.views.generic import TemplateView
    @@ -70,7 +71,7 @@ def post(self, request, *args, **kwargs):  # doccov: ignore
             if command:
                 dispatcher = getattr(self, "dispatch_%s" % command, None)
                 if not callable(dispatcher):
    -                raise Problem(_("Unknown command: `%s`.") % command)
    +                raise Problem(_("Unknown command: `%s`.") % escape(command))
                 dispatch_kwargs = dict(request.POST.items())
                 rv = dispatcher(**dispatch_kwargs)
                 if rv:
    
  • shuup/xtheme/views/extra.py+2 1 modified
    @@ -8,6 +8,7 @@
     from django.core.exceptions import ImproperlyConfigured
     from django.core.signals import setting_changed
     from django.http.response import HttpResponseNotFound
    +from django.utils.html import escape
     
     from shuup.xtheme._theme import get_current_theme
     
    @@ -56,6 +57,6 @@ def extra_view_dispatch(request, view):
         theme = getattr(request, "theme", None) or get_current_theme(request.shop)
         view_func = get_view_by_name(theme, view)
         if not view_func:
    -        msg = "Error! %s/%s: Not found." % (getattr(theme, "identifier", None), view)
    +        msg = "Error! %s/%s: Not found." % (getattr(theme, "identifier", None), escape(view))
             return HttpResponseNotFound(msg)
         return view_func(request)
    

Vulnerability mechanics

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

References

5

News mentions

0

No linked articles in our index yet.