CVE-2019-25095: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability, which was classified as problematic, was found in kakwa LdapCherry up to 0.x. Affected is an unknown function of the component URL Handler. The manipulation leads to cross site scripting. It is possible to launch the attack remotely. Upgrading to version 1.0.0 is able to address this issue. The patch is identified as 6f98076281e9452fdb1adcd1bcbb70a6f968ade9. It is recommended to upgrade the affected component. VDB-217434 is the identifier assigned to this vulnerability.
CWE 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.
CVSS v3 base score: 6.1
Published: 2023-01-05T08:15Z
Repository: https://github.com/kakwa/ldapcherry
Language: Other
Files changed: ldapcherry/__init__.py, resources/templates/adduser.tmpl, resources/templates/login.tmpl, resources/templates/modify.tmpl, resources/templates/selfmodify.tmpl
Commit date: 2019-01-02 14:31:10 -0600
Commit message: Protect against XSS vulnerabilities in URL redirection

- Switch from base64 to URL encoding for the passing the URL, using the built-in Mako filtering
- Apply HTML filtering to Mako output by default
- Disable HTML filtering for nested templates in adduser, modify, and selfmodify

Diff: diff --git a/ldapcherry/__init__.py b/ldapcherry/__init__.py
index 60ce654..a7d5c18 100644
--- a/ldapcherry/__init__.py
+++ b/ldapcherry/__init__.py
@@ -13,11 +13,11 @@ import traceback
 import json
 import logging
 import logging.handlers
 from operator import itemgetter
 from socket import error as socket_error
-import base64
+import urllib
 import cgi
 
 from exceptions import *
 from ldapcherry.lclogging import *
 from ldapcherry.roles import Roles
@@ -385,11 +385,12 @@ class LdapCherry(object):
                 {'dir': self.template_dir},
             severity=logging.DEBUG
         )
         # preload templates
         self.temp_lookup = lookup.TemplateLookup(
-            directories=self.template_dir, input_encoding='utf-8'
+            directories=self.template_dir, input_encoding='utf-8',
+            default_filters=['unicode', 'h']
             )
         # load each template
         self.temp = {}
         for t in ('index.tmpl', 'error.tmpl', 'login.tmpl', '404.tmpl',
                   'searchadmin.tmpl', 'searchuser.tmpl', 'adduser.tmpl',
@@ -571,11 +572,11 @@ class LdapCherry(object):
             return 'anonymous'
         return cherrypy.session.get(SESSION_KEY)
 
     def _check_auth(self, must_admin, redir_login=True):
         """ check if a user is autheticated and, optionnaly an administrator
-        if user not authentifaced -> redirection to login page (with base64
+        if user not authenticated -> redirect to login page (with escaped URL
             of the originaly requested page (redirection after login)
         if user authenticated, not admin and must_admin enabled -> 403 error
         @boolean must_admin: flag "user must be an administrator to access
             this page"
         @rtype str: login of the user
@@ -586,17 +587,17 @@ class LdapCherry(object):
 
         if cherrypy.request.query_string == '':
             qs = ''
         else:
             qs = '?' + cherrypy.request.query_string
-        # base64 of the requested URL
-        b64requrl = base64.b64encode(cherrypy.url() + qs)
+        # Escaped version of the requested URL
+        quoted_requrl = urllib.quote_plus(cherrypy.url() + qs)
         if not username:
-            # return to login page (with base64 of the url in query string
+            # return to login page (with quoted url in query string)
             if redir_login:
                 raise cherrypy.HTTPRedirect(
-                    "/signin?url=%(url)s" % {'url': b64requrl},
+                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                     )
             else:
                 raise cherrypy.HTTPError(
                     "403 Forbidden",
                     "You must be logged in to access this ressource.",
@@ -604,11 +605,11 @@ class LdapCherry(object):
 
         if 'connected' not in cherrypy.session \
                 or not cherrypy.session['connected']:
             if redir_login:
                 raise cherrypy.HTTPRedirect(
-                    "/signin?url=%(url)s" % {'url': b64requrl},
+                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                     )
             else:
                 raise cherrypy.HTTPError(
                     "403 Forbidden",
                     "You must be logged in to access this ressource.",
@@ -629,11 +630,11 @@ class LdapCherry(object):
                 cherrypy.session['isadmin']:
             return username
         else:
             if redir_login:
                 raise cherrypy.HTTPRedirect(
-                    "/signin?url=%(url)s" % {'url': b64requrl},
+                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                     )
             else:
                 raise cherrypy.HTTPError(
                     "403 Forbidden",
                     "You must be logged in to access this ressource.",
@@ -917,11 +918,11 @@ class LdapCherry(object):
             )
             cherrypy.session[SESSION_KEY] = cherrypy.request.login = login
             if url is None:
                 redirect = "/"
             else:
-                redirect = base64.b64decode(url)
+                redirect = url
             raise cherrypy.HTTPRedirect(redirect)
         else:
             message = "login failed for user '%(user)s'" % {
                 'user': login
             }
@@ -930,11 +931,11 @@ class LdapCherry(object):
                 severity=logging.WARNING
             )
             if url is None:
                 qs = ''
             else:
-                qs = '?url=' + url
+                qs = '?url=' + urllib.quote_plus(url)
             raise cherrypy.HTTPRedirect("/signin" + qs)
 
     @cherrypy.expose
     @exception_decorator
     def logout(self):
diff --git a/resources/templates/adduser.tmpl b/resources/templates/adduser.tmpl
index 7b690fc..8b27f75 100644
--- a/resources/templates/adduser.tmpl
+++ b/resources/templates/adduser.tmpl
@@ -7,15 +7,15 @@
         <div class="col-md-12 column">
             <div class="well well-sm">
               <form method='POST' autocomplete="off" action='/adduser' role="form" class="form-signin" id=form>
               <fieldset>
               <legend>Fill new user's attributes:</legend>
-              ${form}
+              ${form | n}
               </fieldset>
               <fieldset>
               <legend>Enable/Disable user's roles:</legend>
-              ${roles}
+              ${roles | n}
               </fieldset>
               <div class="form-group">
                 <div class="input-group">
                   <button class="btn btn-default green" type="submit">
                   <span class="glyphicon glyphicon-plus-sign"></span> Add User</button>
diff --git a/resources/templates/login.tmpl b/resources/templates/login.tmpl
index d325702..3bc6ca0 100644
--- a/resources/templates/login.tmpl
+++ b/resources/templates/login.tmpl
@@ -2,17 +2,17 @@
 <%inherit file="base.tmpl"/>
 <%block name="core">
     <div class="row clearfix" style="margin-top:30px">
         <div class="col-md-4 column"></div>
         <div class="col-md-4 column well">
-<%
-if url is None:
-    qs=''
-else:
-    qs='?url=' + url
-%>
-            <form method='POST' action='/login${qs}' role="form" class="form-signin">
+            <form method='POST' role="form" class="form-signin"
+% if url:
+                action='login?url=${url | u}'
+% else:
+                action='login'
+% endif
+            >
               <div class="form-group">
               <h2 class="form-signin-heading">Please sign in</h2>
                 <div class="input-group">
                     <span class="input-group-addon glyphicon glyphicon-user"></span>
                     <input type="text" class="form-control" name="login" placeholder="Login" required autofocus>
diff --git a/resources/templates/modify.tmpl b/resources/templates/modify.tmpl
index dfeab49..b494fcc 100644
--- a/resources/templates/modify.tmpl
+++ b/resources/templates/modify.tmpl
@@ -7,15 +7,15 @@
         <div class="col-md-12 column">
             <div class="well well-sm">
               <form method='POST' action='/modify' role="form" class="form-signin" id="form">
               <fieldset>
               <legend>Modify user's attributes:</legend>
-              ${form}
+              ${form | n}
               </fieldset>
               <fieldset>
               <legend>Enable/Disable user's roles:</legend>
-              ${roles}
+              ${roles | n}
               </fieldset>
               % if len(standalone_groups) != 0:
               <fieldset>
               <legend>Delete user's individual groups:</legend>
               <table id="RecordTable" class="table table-hover table-condensed tablesorter">
diff --git a/resources/templates/selfmodify.tmpl b/resources/templates/selfmodify.tmpl
index ee58c77..95bd806 100644
--- a/resources/templates/selfmodify.tmpl
+++ b/resources/templates/selfmodify.tmpl
@@ -6,11 +6,11 @@
         </div>
         <div class="col-md-12 column">
             <div class="well well-sm">
               <form method='POST' action='/selfmodify' autocomplete="off" role="form" class="form-signin" id="form">
               <legend>Modify your attributes:</legend>
-              ${form}
+              ${form | n}
               </fieldset>
               <div class="form-group">
                 <div class="input-group">
                     <button type="submit" class="btn btn-default blue">
                     <span class="glyphicon glyphicon-cog"></span> Modify</button>

Vulnerable code: import base64
            directories=self.template_dir, input_encoding='utf-8'
        if user not authentifaced -> redirection to login page (with base64
        # base64 of the requested URL
        b64requrl = base64.b64encode(cherrypy.url() + qs)
            # return to login page (with base64 of the url in query string
                    "/signin?url=%(url)s" % {'url': b64requrl},
                    "/signin?url=%(url)s" % {'url': b64requrl},
                    "/signin?url=%(url)s" % {'url': b64requrl},
                redirect = base64.b64decode(url)
                qs = '?url=' + url
              ${form}
              ${roles}
<%
if url is None:
    qs=''
else:
    qs='?url=' + url
%>
            <form method='POST' action='/login${qs}' role="form" class="form-signin">
              ${form}
              ${roles}
              ${form}
Fixed code: import urllib
            directories=self.template_dir, input_encoding='utf-8',
            default_filters=['unicode', 'h']
        if user not authenticated -> redirect to login page (with escaped URL
        # Escaped version of the requested URL
        quoted_requrl = urllib.quote_plus(cherrypy.url() + qs)
            # return to login page (with quoted url in query string)
                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                    "/signin?url=%(url)s" % {'url': quoted_requrl},
                redirect = url
                qs = '?url=' + urllib.quote_plus(url)
              ${form | n}
              ${roles | n}
            <form method='POST' role="form" class="form-signin"
% if url:
                action='login?url=${url | u}'
% else:
                action='login'
% endif
            >
              ${form | n}
              ${roles | n}
              ${form | n}