CVE-2021-25963 (published 2021-09-30T08:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-79
cwe_name: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
extended_description: 
            Cross-site scripting (XSS) vulnerabilities occur when:
            
               Untrusted data enters a web application, typically from a web request.
               The web application dynamically generates a web page that contains this untrusted data.
               During page generation, the application does not prevent the data from containing content that is executable by a web browser, such as JavaScript, HTML tags, HTML attributes, mouse events, Flash, ActiveX, etc.
               A victim visits the generated web page through a web browser, which contains malicious script that was injected using the untrusted data.
               Since the script comes from a web page that was sent by the web server, the victim's web browser executes the malicious script in the context of the web server's domain.
               This effectively violates the intention of the web browser's same-origin policy, which states that scripts in one domain should not be able to access resources or run code in a different domain.
            
            There are three main kinds of XSS:
            
               
                  Type 1: Reflected XSS (or Non-Persistent) - 
         			The server reads data directly from the HTTP request and reflects it back in the HTTP response. Reflected XSS exploits occur when an attacker causes a victim to supply dangerous content to a vulnerable web application, which is then reflected back to the victim and executed by the web browser. The most common mechanism for delivering malicious content is to include it as a parameter in a URL that is posted publicly or e-mailed directly to the victim. URLs constructed in this manner constitute the core of many phishing schemes, whereby an attacker convinces a victim to visit a URL that refers to a vulnerable site. After the site reflects the attacker's content back to the victim, the content is executed by the victim's browser.
               
                  Type 2: Stored XSS (or Persistent) - 
               The application stores dangerous data in a database, message forum, visitor log, or other trusted data store. At a later time, the dangerous data is subsequently read back into the application and included in dynamic content. From an attacker's perspective, the optimal place to inject malicious content is in an area that is displayed to either many users or particularly interesting users. Interesting users typically have elevated privileges in the application or interact with sensitive data that is valuable to the attacker. If one of these users executes malicious content, the attacker may be able to perform privileged operations on behalf of the user or gain access to sensitive data belonging to the user. For example, the attacker might inject XSS into a log message, which might not be handled properly when an administrator views the logs.
            
               
                  Type 0: DOM-Based XSS - 
               In DOM-based XSS, the client performs the injection of XSS into the page; in the other types, the server performs the injection. DOM-based XSS generally involves server-controlled, trusted script that is sent to the client, such as Javascript that performs sanity checks on a form before the user submits it. If the server-supplied script processes user-supplied data and then injects it back into the web page (such as with dynamic HTML), then DOM-based XSS is possible.
            
            
            Once the malicious script is injected, the attacker can perform a variety of malicious activities. The attacker could transfer private information, such as cookies that may include session information, from the victim's machine to the attacker. The attacker could send malicious requests to a web site on behalf of the victim, which could be especially dangerous to the site if the victim has administrator privileges to manage that site. Phishing attacks could be used to emulate trusted web sites and trick the victim into entering a password, allowing the attacker to compromise the victim's account on that web site. Finally, the script could exploit a vulnerability in the web browser itself possibly taking over the victim's machine, sometimes referred to as "drive-by hacking."
            In many cases, the attack can be launched without the victim even being aware of it. Even with careful users, attackers frequently use a variety of methods to encode the malicious portion of the attack, such as URL encoding or Unicode, so the request looks less suspicious.
         
         

Fix information:
repo_url: https://github.com/shuup/shuup
commit_details: repo_url: https://github.com/shuup/shuup
author: Christian Hess
author_date: 2021-07-06 20:26:15-03:00
msg: General: fix critical views that can be subject of XSS attacks

The views were returning not escaped content which is unsafe

Thanks to Daniel Elkabes who reported the issue
repository: repo_url: https://github.com/shuup/shuup
repo_name: shuup/shuup
description: E-Commerce Platform
repo_language: Python
owner: shuup
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Markdown
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -16,6 +16,7 @@ from django.contrib.auth.views import redirect_to_login
 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 @@ class AdminRegexURLPattern(URLPattern):
             # 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...")
                 )
             )

programming_language: Python
filename: command_dispatcher.py
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ import six
 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 @@ class BasketCommandDispatcher(object):
         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)

programming_language: Python
filename: _process.py
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@ from __future__ import unicode_literals
 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 @@ class CheckoutProcess(object):
                     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

programming_language: Python
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@ from __future__ import unicode_literals
 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 @@ from .views.upload import media_upload
 
 
 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

programming_language: Python
filename: excs.py
change_type: ModificationType.MODIFY
diff: @@ -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))

programming_language: Python
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,7 @@ from shuup.xtheme.views.plugins import (
 
 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+)/$",

programming_language: Python
filename: command.py
change_type: ModificationType.MODIFY
diff: @@ -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))

programming_language: Python
filename: editor.py
change_type: ModificationType.MODIFY
diff: @@ -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 @@ class EditorView(TemplateView):
         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:

programming_language: Python
filename: extra.py
change_type: ModificationType.MODIFY
diff: @@ -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)

programming_language: Python