CVE-2023-29207: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value:  XWiki Commons are technical libraries common to several other top level XWiki projects. The Livetable Macro wasn't properly sanitizing column names, thus allowing the insertion of raw HTML code including JavaScript. This vulnerability was also exploitable via the Documents Macro that is included since XWiki 3.5M1 and doesn't require script rights, this can be demonstrated with the syntax `{{documents id="example" count="5" actions="false" columns="doc.title, before<script>alert(1)</script>after"/}}`. Therefore, this can also be exploited by users without script right and in comments. With the interaction of a user with more rights, this could be used to execute arbitrary actions in the wiki, including privilege escalation, remote code execution, information disclosure, modifying or deleting content. This has been patched in XWiki 14.9, 14.4.6, and 13.10.10.

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: 9
Published: 2023-04-15T16:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Other
Files changed: xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/macros.vm, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
Commit date: 2022-10-18 16:32:12 +0200
Commit message: XWIKI-15205: Improve escaping in LiveTable

Diff: diff --git a/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/macros.vm b/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/macros.vm
index 55dc1adf7d0df..22e775b302a3e 100644
--- a/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/macros.vm
+++ b/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/macros.vm
@@ -231,16 +231,16 @@
               #set ($isSortable = $colprop.sortable != false && $colname != '_actions' && $colprop.type != 'hidden')
               ## Get the column's display name to use in the header.
               #set($displayName = "#getLivetableColumnDisplayName($colname, $colprop, $transprefix)")
               #set ($defaultHeaderClass = {'_actions': 'actions', '_avatar': 'avatar'})
               #set ($discard = $colprop.putIfAbsent('headerClass', $defaultHeaderClass.get($colname)))
-              <th scope="col" class="xwiki-livetable-display-header-text $!colprop.headerClass
+              <th scope="col" class="xwiki-livetable-display-header-text $!escapetool.xml($colprop.headerClass)
                   #if ($!colprop.type == 'hidden')hidden#end
                   #if ($isSortable)sortable #if ($colname == $selectedColumn)selected $defaultOrder#else asc#end#end">
                 #if($isFilterable)<label for="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}">#end
-                #if($isSortable)<a data-rel="${colname}">#end
-                $displayName
+                #if($isSortable)<a data-rel="${escapetool.xml($colname)}">#end
+                $escapetool.xml($displayName)
                 #if($isSortable)</a>#end
                 #if($isFilterable)</label>#end
                 #set($columnCount = $mathtool.add($columnCount, 1))
               </th>
             #end
@@ -308,39 +308,39 @@
       ## filter in this case because it is the most generic.
       #set ($filterType = 'text')
     #end
   #end
   #if ($filterType == 'list' || $filterType == 'multilist')
-    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column"
+    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)"
       #if ($filterType == 'multilist')
         #set ($discard = $xwiki.jsfx.use('js/xwiki/table/livetablemulti.js', true))
         #set ($discard = $xwiki.linkx.use($services.webjars.url('bootstrap-select', 'css/bootstrap-select.css'),
           {'type': 'text/css', 'rel': 'stylesheet'}))
         class="xwiki-livetable-multilist" multiple="multiple">
       #else
-        ><option value="">$services.localization.render('platform.livetable.selectAll')</option>
+        ><option value="">$escapetool.xml($services.localization.render('platform.livetable.selectAll'))</option>
         <option disabled="disabled">────</option>
       #end
     #set ($storedValues = $xproperty.listValues)
     #set ($displayedValues = $xproperty.mapValues)
     #foreach ($storedValue in $storedValues)
       #set ($l10nKey = "${xproperty.className}_${column}_$storedValue")
       #set ($displayedValue = $services.localization.render($l10nKey))
       #if ($displayedValue == $l10nKey)
         #set ($displayedValue = $displayedValues.get($storedValue).value)
       #end
-      <option value="$storedValue">$displayedValue</option>
+      <option value="$!escapetool.xml($storedValue)">$!escapetool.xml($displayedValue)</option>
     #end
     </select>
   #elseif ($filterType == 'boolean')
-    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column">
+    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)">
       <option value="">$services.localization.render('platform.livetable.selectAll')</option>
       <option value="0">$services.localization.render('no')</option>
       <option value="1">$services.localization.render('yes')</option>
     </select>
   #elseif ($filterType == 'text' || $filterType == 'number')
-    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column" type="text"
+    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)" type="text"
       #if ("$!columnProperties.size" != '')size="$!escapetool.xml($columnProperties.size)"#end
       title="$escapetool.xml($services.localization.render('platform.livetable.filtersTitle',
         [$services.localization.render("${transprefix}$column")]))" />
   #elseif ($filterType == 'date')
     #set ($discard = $xwiki.jsfx.use('js/xwiki/table/livetabledate.js', true))
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
index 2910d7425f0b8..1bb23b17f890f 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
@@ -1469,16 +1469,16 @@ $Msz MB##
               #set ($isSortable = $colprop.sortable != false && $colname != '_actions' && $colprop.type != 'hidden')
               ## Get the column's display name to use in the header.
               #set($displayName = "#getLivetableColumnDisplayName($colname, $colprop, $transprefix)")
               #set ($defaultHeaderClass = {'_actions': 'actions', '_avatar': 'avatar'})
               #set ($discard = $colprop.putIfAbsent('headerClass', $defaultHeaderClass.get($colname)))
-              <th scope="col" class="xwiki-livetable-display-header-text $!colprop.headerClass
+              <th scope="col" class="xwiki-livetable-display-header-text $!escapetool.xml($colprop.headerClass)
                   #if ($!colprop.type == 'hidden')hidden#end
                   #if ($isSortable)sortable #if ($colname == $selectedColumn)selected $defaultOrder#else asc#end#end">
-                #if($isSortable)<a rel="${colname}">#end
+                #if($isSortable)<a rel="${escapetool.xml($colname)}">#end
                 #if($isFilterable)<label for="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}">#end
-                $displayName
+                $escapetool.xml($displayName)
                 #if($isFilterable)</label>#end
                 #if($isSortable)</a>#end
                 #set($columnCount = $mathtool.add($columnCount, 1))
               </th>
             #end
@@ -1571,36 +1571,36 @@ $Msz MB##
     #end
   #end
   #set($escapedTitle = $escapetool.xml($services.localization.render('platform.livetable.filtersTitle',
     [$services.localization.render("${transprefix}$column")])))
   #if ($filterType == 'list' || $filterType == 'multilist')
-    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column"
+    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)"
       #if ($filterType == 'multilist')
         class="xwiki-livetable-multilist" multiple="multiple">
       #else
-        ><option value="">$services.localization.render('platform.livetable.selectAll')</option>
+        ><option value="">$escapetool.xml($services.localization.render('platform.livetable.selectAll'))</option>
         <option disabled="disabled">────</option>
       #end
     #set ($storedValues = $xproperty.listValues)
     #set ($displayedValues = $xproperty.mapValues)
     #foreach ($storedValue in $storedValues)
       #set ($l10nKey = "${xproperty.className}_${column}_$storedValue")
       #set ($displayedValue = $services.localization.render($l10nKey))
       #if ($displayedValue == $l10nKey)
         #set ($displayedValue = $displayedValues.get($storedValue).value)
       #end
-      <option value="$storedValue">$displayedValue</option>
+      <option value="$!escapetool.xml($storedValue)">$!escapetool.xml($displayedValue)</option>
     #end
     </select>
   #elseif ($filterType == 'boolean')
-    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column">
+    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)">
       <option value="">$services.localization.render('platform.livetable.selectAll')</option>
       <option value="0">$services.localization.render('no')</option>
       <option value="1">$services.localization.render('yes')</option>
     </select>
   #elseif ($filterType == 'text' || $filterType == 'number')
-    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column" type="text"
+    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)" type="text"
       #if ("$!columnProperties.size" != '')size="$!escapetool.xml($columnProperties.size)"#end
       title="$escapedTitle"/>
   #elseif ($filterType == 'suggest' && $xproperty)
     #set ($discard = $xwiki.linkx.use($services.webjars.url('selectize.js', 'css/selectize.css'),
       {'type': 'text/css', 'rel': 'stylesheet'}))

Vulnerable code:               <th scope="col" class="xwiki-livetable-display-header-text $!colprop.headerClass
                #if($isSortable)<a data-rel="${colname}">#end
                $displayName
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column"
        ><option value="">$services.localization.render('platform.livetable.selectAll')</option>
      <option value="$storedValue">$displayedValue</option>
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column">
    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column" type="text"
              <th scope="col" class="xwiki-livetable-display-header-text $!colprop.headerClass
                #if($isSortable)<a rel="${colname}">#end
                $displayName
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column"
        ><option value="">$services.localization.render('platform.livetable.selectAll')</option>
      <option value="$storedValue">$displayedValue</option>
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column">
    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$column" type="text"
Fixed code:               <th scope="col" class="xwiki-livetable-display-header-text $!escapetool.xml($colprop.headerClass)
                #if($isSortable)<a data-rel="${escapetool.xml($colname)}">#end
                $escapetool.xml($displayName)
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)"
        ><option value="">$escapetool.xml($services.localization.render('platform.livetable.selectAll'))</option>
      <option value="$!escapetool.xml($storedValue)">$!escapetool.xml($displayedValue)</option>
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)">
    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)" type="text"
              <th scope="col" class="xwiki-livetable-display-header-text $!escapetool.xml($colprop.headerClass)
                #if($isSortable)<a rel="${escapetool.xml($colname)}">#end
                $escapetool.xml($displayName)
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)"
        ><option value="">$escapetool.xml($services.localization.render('platform.livetable.selectAll'))</option>
      <option value="$!escapetool.xml($storedValue)">$!escapetool.xml($displayedValue)</option>
    <select id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)">
    <input id="xwiki-livetable-${htmlLiveTableId}-filter-${foreach.count}" name="$!escapetool.xml($column)" type="text"