CVE-2023-26476: Improper Restriction of Excessive Authentication Attempts (CWE-307)
value: XWiki Platform is a generic wiki platform. Starting in version 3.2-m3, users can deduce the content of the password fields by repeated call to `LiveTableResults` and `WikisLiveTableResultsMacros`. The issue can be fixed by upgrading to versions 14.7-rc-1, 13.4.4, or 13.10.9 and higher, or in version >= 3.2M3 by applying the patch manually on `LiveTableResults` and `WikisLiveTableResultsMacros`.
CWE description: The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
CVSS v3 base score: 7.5
Published: 2023-03-02T19:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml, xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/test/java/org/xwiki/livetable/LiveTableResultsTest.java, xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
Commit date: 2022-08-18 11:50:36 +0200
Commit message: XWIKI-19949: Livetable results allow reconstructing password hashes using 768 requests

Diff: diff --git a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
index ced0d6642b1e4..c2162cafe9831 100644
--- a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
@@ -71,11 +71,19 @@
   ##
   ## Add the columns needed for the actual data
   ##
   #set($tablelist = [])
   #foreach($colname in $collist)
-    #livetable_addColumnToQuery($colname)
+    ## If a classname is defined and the class field corresponding to the column name,
+    ## we check the type of the field and skip it if it's Password.
+    #if ($className != '' &amp;&amp; $class.get($colname))
+      #if ($class.get($colname).classType != 'Password')
+        #livetable_addColumnToQuery($colname)
+      #end
+    #else
+      #livetable_addColumnToQuery($colname)
+    #end
   #end
   ##
   ## Tag filtering
   ##
   #if($request.tag)
diff --git a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/test/java/org/xwiki/livetable/LiveTableResultsTest.java b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/test/java/org/xwiki/livetable/LiveTableResultsTest.java
index cf2ae0bd5f4d6..64d7cc9c12713 100644
--- a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/test/java/org/xwiki/livetable/LiveTableResultsTest.java
+++ b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/test/java/org/xwiki/livetable/LiveTableResultsTest.java
@@ -18,10 +18,11 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.livetable;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.lang3.StringUtils;
@@ -42,10 +43,11 @@
 import org.xwiki.test.page.XWikiSyntax20ComponentList;
 import org.xwiki.velocity.tools.JSONTool;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.classes.BaseClass;
 import com.xpn.xwiki.objects.classes.StaticListClass;
 import com.xpn.xwiki.plugin.tag.TagPluginApi;
 
 import static java.util.Collections.emptyList;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -498,10 +500,39 @@ void removeObfuscatedResultsWhenLimitIs0() throws Exception
         assertEquals(0, getRowCount());
         assertEquals(1, getOffset());
         assertEquals(emptyList(), getRows());
     }
 
+    @Test
+    void cleanupAccessToPasswordFields() throws Exception
+    {
+        // Initialize an XClass with a password field.
+        DocumentReference documentReference = new DocumentReference("xwiki", "XWiki", "MyClass");
+        XWikiDocument xwikiDocument = this.xwiki.getDocument(documentReference, this.context);
+        BaseClass xClass = xwikiDocument.getXClass();
+        xClass.addPasswordField("password", "Password", 30);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+
+        when(this.queryService.hql(anyString())).thenReturn(this.query);
+        when(this.query.setLimit(anyInt())).thenReturn(this.query);
+        when(this.query.setOffset(anyInt())).thenReturn(this.query);
+        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
+        when(this.query.count()).thenReturn(0L);
+        when(this.query.execute()).thenReturn(Collections.emptyList());
+
+        this.request.put("classname", "XWiki.MyClass");
+        this.request.put("password", "abcd");
+        this.request.put("collist", "password");
+
+        renderPage();
+
+        verify(this.queryService).hql(", BaseObject as obj   "
+            + "where obj.name=doc.fullName "
+            + "and obj.className = :className "
+            + "and doc.fullName not in (:classTemplate1, :classTemplate2)  ");
+    }
+
     //
     // Helper methods
     //
 
     @SuppressWarnings("unchecked")
diff --git a/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml b/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
index 67d3f43f974b3..349b4aa2e5b26 100644
--- a/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
@@ -55,11 +55,19 @@
   ##
   ## Add the columns needed for the actual data
   ##
   #set($tablelist = [])
   #foreach($colname in $collist)
-    #livetable_addColumnToQuery($colname)
+    ## If a classname is defined and the class field corresponding to the column name,
+    ## we check the type of the field and skip it if it's Password.
+    #if ($className != '' &amp;&amp; $class.get($colname))
+      #if ($class.get($colname).classType != 'Password')
+        #livetable_addColumnToQuery($colname)
+      #end
+    #else
+      #livetable_addColumnToQuery($colname)
+    #end
   #end
   ##
   ## Tag filtering
   ##
   #if($request.tag)

Vulnerable code:     #livetable_addColumnToQuery($colname)
    #livetable_addColumnToQuery($colname)
Fixed code:     ## If a classname is defined and the class field corresponding to the column name,
    ## we check the type of the field and skip it if it's Password.
    #if ($className != '' &amp;&amp; $class.get($colname))
      #if ($class.get($colname).classType != 'Password')
        #livetable_addColumnToQuery($colname)
      #end
    #else
      #livetable_addColumnToQuery($colname)
    #end
import java.util.Collections;
import com.xpn.xwiki.objects.classes.BaseClass;
    @Test
    void cleanupAccessToPasswordFields() throws Exception
    {
        // Initialize an XClass with a password field.
        DocumentReference documentReference = new DocumentReference("xwiki", "XWiki", "MyClass");
        XWikiDocument xwikiDocument = this.xwiki.getDocument(documentReference, this.context);
        BaseClass xClass = xwikiDocument.getXClass();
        xClass.addPasswordField("password", "Password", 30);
        this.xwiki.saveDocument(xwikiDocument, this.context);

        when(this.queryService.hql(anyString())).thenReturn(this.query);
        when(this.query.setLimit(anyInt())).thenReturn(this.query);
        when(this.query.setOffset(anyInt())).thenReturn(this.query);
        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
        when(this.query.count()).thenReturn(0L);
        when(this.query.execute()).thenReturn(Collections.emptyList());

        this.request.put("classname", "XWiki.MyClass");
        this.request.put("password", "abcd");
        this.request.put("collist", "password");

        renderPage();

        verify(this.queryService).hql(", BaseObject as obj   "
            + "where obj.name=doc.fullName "
            + "and obj.className = :className "
            + "and doc.fullName not in (:classTemplate1, :classTemplate2)  ");
    }

    ## If a classname is defined and the class field corresponding to the column name,
    ## we check the type of the field and skip it if it's Password.
    #if ($className != '' &amp;&amp; $class.get($colname))
      #if ($class.get($colname).classType != 'Password')
        #livetable_addColumnToQuery($colname)
      #end
    #else
      #livetable_addColumnToQuery($colname)
    #end