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.
| Package | Affected versions | Patched versions |
|---|---|---|
shuupPyPI | >= 1.6.0, < 2.11.0 | 2.11.0 |
Affected products
1Patches
175714c37e327General: fix critical views that can be subject of XSS attacks
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- github.com/advisories/GHSA-5pcx-vqjp-p34wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-25963ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/shuup/PYSEC-2021-350.yamlghsaWEB
- github.com/shuup/shuup/commit/75714c37e32796eb7cbb0d977af5bcaa26573588ghsax_refsource_MISCWEB
- www.whitesourcesoftware.com/vulnerability-database/CVE-2021-25963ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.