This is an automated email from the ASF dual-hosted git repository.

OpenPj pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/manifoldcf.git

commit 6ac22303e78c4de2886e81b9b5d643c34028e8b3
Author: Piergiorgio Lucidi <[email protected]>
AuthorDate: Mon Oct 5 12:08:20 2026 +0200

    Validate sortcolumn parameters in repository connection queue and history 
reports.
        - Add identifier and allowlist checks for sort columns in SortOrder, 
JobManager, RepositoryHistoryManager, and API endpoints.
        - Add unit and database-level validation tests.
---
 .../manifoldcf/crawler/interfaces/SortOrder.java   |   6 +
 .../apache/manifoldcf/crawler/jobs/JobManager.java |  42 +++-
 .../repository/RepositoryHistoryManager.java       |  48 +++-
 .../manifoldcf/crawler/system/ManifoldCF.java      | 136 +++++++++--
 .../manifoldcf/crawler/tests/SanityHSQLDBTest.java |  61 ++++-
 .../manifoldcf/crawler/tests/SortOrderTest.java    | 264 +++++++++++++++++++++
 6 files changed, 520 insertions(+), 37 deletions(-)

diff --git 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/interfaces/SortOrder.java
 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/interfaces/SortOrder.java
index 4858491b1..259a48d6c 100644
--- 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/interfaces/SortOrder.java
+++ 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/interfaces/SortOrder.java
@@ -96,6 +96,8 @@ public class SortOrder
   */
   public void clickColumn(String columnName)
   {
+    if (columnName == null || !columnName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$"))
+      return;
     int findIndex = -1;
     int i = 0;
     while (i < sortList.size())
@@ -185,6 +187,8 @@ public class SortOrder
 
     public SortSpec(String column, int direction)
     {
+      if (column == null || !column.matches("^[a-zA-Z_][a-zA-Z0-9_]*$"))
+        throw new IllegalArgumentException("Invalid sort column name: " + 
column);
       this.column = column;
       this.direction = direction;
     }
@@ -216,6 +220,8 @@ public class SortOrder
         sb.append(y);
       }
       column = sb.toString();
+      if (!column.matches("^[a-zA-Z_][a-zA-Z0-9_]*$"))
+        throw new ManifoldCFException("Invalid sort column name: " + column);
     }
 
     public String getColumn()
diff --git 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/jobs/JobManager.java
 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/jobs/JobManager.java
index 3a7d1fea6..0a737f155 100644
--- 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/jobs/JobManager.java
+++ 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/jobs/JobManager.java
@@ -9346,7 +9346,7 @@ public class JobManager implements IJobManager
     
     addCriteria(sb,list,"t0.",connectionName,filterCriteria,true);
     // The intrinsic ordering is provided by the "id" column, and nothing else.
-    addOrdering(sb,new String[]{"id"},sortOrder);
+    addOrdering(sb,validDocumentStatusColumns,new String[]{"id"},sortOrder);
     addLimits(sb,startRow,rowCount);
     return database.performQuery(sb.toString(),list,null,null,rowCount,null);
   }
@@ -9499,7 +9499,7 @@ public class JobManager implements IJobManager
 
     addCriteria(sb,list,"",connectionName,filterCriteria,false);
     sb.append(") t1 GROUP BY idbucket");
-    addOrdering(sb,new 
String[]{"idbucket","inactive","processing","expiring","deleting","processready","expireready","processwaiting","expirewaiting","waitingforever","hopcountexceeded"},sortOrder);
+    addOrdering(sb,validQueueStatusColumns,validQueueStatusColumns,sortOrder);
     addLimits(sb,startRow,rowCount);
     return database.performQuery(sb.toString(),list,null,null,rowCount,null);
   }
@@ -9702,12 +9702,26 @@ public class JobManager implements IJobManager
     return true;
   }
 
+  protected static final String[] validDocumentStatusColumns = new String[]{
+    
"identifier","job","state","status","scheduled","action","retrycount","retrylimit","id","docid","checktime","failcount","failtime"
+  };
+  protected static final String[] validQueueStatusColumns = new String[]{
+    
"idbucket","inactive","processing","expiring","deleting","processready","expireready","processwaiting","expirewaiting","waitingforever","hopcountexceeded"
+  };
+
   /** Add ordering.
   */
-  protected void addOrdering(StringBuilder sb, String[] completeFieldList, 
SortOrder sort)
+  protected void addOrdering(StringBuilder sb, String[] validFieldList, 
String[] completeFieldList, SortOrder sort)
+    throws ManifoldCFException
   {
+    Map<String,String> validMap = new HashMap<String,String>();
+    for (String validCol : validFieldList)
+    {
+      validMap.put(validCol.toLowerCase(Locale.ROOT), validCol);
+    }
+
     // Keep track of the fields we've seen
-    Map hash = new HashMap();
+    Map<String,String> hash = new HashMap<String,String>();
 
     // Emit the "Order by"
     sb.append(" ORDER BY ");
@@ -9717,11 +9731,16 @@ public class JobManager implements IJobManager
     int count = sort.getCount();
     while (i < count)
     {
+      String rawColumn = sort.getColumn(i);
+      String column = validMap.get(rawColumn.toLowerCase(Locale.ROOT));
+      if (column == null)
+      {
+        throw new ManifoldCFException("Unknown or invalid sort column: '" + 
rawColumn + "'");
+      }
       if (i > 0)
         sb.append(",");
-      String column = sort.getColumn(i);
       sb.append(column);
-      if (sort.getDirection(i) == sort.SORT_ASCENDING)
+      if (sort.getDirection(i) == SortOrder.SORT_ASCENDING)
         sb.append(" ASC");
       else
         sb.append(" DESC");
@@ -9742,16 +9761,19 @@ public class JobManager implements IJobManager
           sb.append(",");
         sb.append(field);
         sb.append(" DESC");
-        //if (j == 0)
-        //  sb.append(" DESC");
-        //else
-        //  sb.append(" ASC");
         i++;
       }
       j++;
     }
   }
 
+  @Deprecated
+  protected void addOrdering(StringBuilder sb, String[] completeFieldList, 
SortOrder sort)
+    throws ManifoldCFException
+  {
+    addOrdering(sb, completeFieldList, completeFieldList, sort);
+  }
+
   /** Add limit and offset.
   */
   protected void addLimits(StringBuilder sb, int startRow, int maxRowCount)
diff --git 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/repository/RepositoryHistoryManager.java
 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/repository/RepositoryHistoryManager.java
index a19f308df..0194ae1e3 100644
--- 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/repository/RepositoryHistoryManager.java
+++ 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/repository/RepositoryHistoryManager.java
@@ -295,7 +295,7 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     addCriteria(sb,list,"",connectionName,criteria,false);
     // Note well: We can't order by "identifier" in all databases, so in order 
to guarantee order we use "id".  This will force a specific internal
     // order for the OFFSET/LIMIT clause.  We include "starttime" because 
that's the default ordering.
-    addOrdering(sb,new String[]{"starttime","id"},sort);
+    addOrdering(sb,validSimpleReportColumns,new 
String[]{"starttime","id"},sort);
     addLimits(sb,startRow,maxRowCount);
     return performQuery(sb.toString(),list,null,null,maxRowCount);
   }
@@ -430,7 +430,7 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     ArrayList newList = new ArrayList();
     newsb.append(constructDistinctOnClause(newList,sb.toString(),list,new 
String[]{"idbucket"},
       new String[]{"activitycount"},new 
boolean[]{false},otherColumns)).append(") t4");
-    addOrdering(newsb,new 
String[]{"activitycount","starttime","endtime","idbucket"},sort);
+    
addOrdering(newsb,validMaxActivityReportColumns,validMaxActivityReportColumns,sort);
     addLimits(newsb,startRow,maxRowCount);
     return performQuery(newsb.toString(),newList,null,null,maxRowCount);
   }
@@ -538,7 +538,7 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     ArrayList newList = new ArrayList();
     newsb.append(constructDistinctOnClause(newList,sb.toString(),list,new 
String[]{"idbucket"},
       new String[]{"bytecount"},new boolean[]{false},otherColumns)).append(") 
t4");
-    addOrdering(newsb,new 
String[]{"bytecount","starttime","endtime","idbucket"},sort);
+    
addOrdering(newsb,validMaxBandwidthReportColumns,validMaxBandwidthReportColumns,sort);
     addLimits(newsb,startRow,maxRowCount);
     return performQuery(newsb.toString(),newList,null,null,maxRowCount);
   }
@@ -567,7 +567,7 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     sb.append(" AS idbucket FROM ").append(getTableName());
     addCriteria(sb,list,"",connectionName,filterCriteria,false);
     sb.append(") t1 GROUP BY resultcodebucket,idbucket");
-    addOrdering(sb,new 
String[]{"eventcount","resultcodebucket","idbucket"},sort);
+    
addOrdering(sb,validResultCodesReportColumns,validResultCodesReportColumns,sort);
     addLimits(sb,startRow,maxRowCount);
     return performQuery(sb.toString(),list,null,null,maxRowCount);
   }
@@ -659,12 +659,32 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     return true;
   }
 
+  protected static final String[] validSimpleReportColumns = new String[]{
+    
"starttime","resultcode","resultdesc","identifier","activity","bytes","elapsedtime","id","entityid","datasize","activitytype","endtime"
+  };
+  protected static final String[] validMaxActivityReportColumns = new String[]{
+    "activitycount","starttime","endtime","idbucket"
+  };
+  protected static final String[] validMaxBandwidthReportColumns = new 
String[]{
+    "bytecount","starttime","endtime","idbucket"
+  };
+  protected static final String[] validResultCodesReportColumns = new String[]{
+    "eventcount","resultcodebucket","idbucket"
+  };
+
   /** Add ordering.
   */
-  protected void addOrdering(StringBuilder sb, String[] completeFieldList, 
SortOrder sort)
+  protected void addOrdering(StringBuilder sb, String[] validFieldList, 
String[] completeFieldList, SortOrder sort)
+    throws ManifoldCFException
   {
+    Map<String,String> validMap = new HashMap<String,String>();
+    for (String validCol : validFieldList)
+    {
+      validMap.put(validCol.toLowerCase(Locale.ROOT), validCol);
+    }
+
     // Keep track of the fields we've seen
-    Map hash = new HashMap();
+    Map<String,String> hash = new HashMap<String,String>();
 
     // Emit the "Order by"
     sb.append(" ORDER BY ");
@@ -674,11 +694,16 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     int count = sort.getCount();
     while (i < count)
     {
+      String rawColumn = sort.getColumn(i);
+      String column = validMap.get(rawColumn.toLowerCase(Locale.ROOT));
+      if (column == null)
+      {
+        throw new ManifoldCFException("Unknown or invalid sort column: '" + 
rawColumn + "'");
+      }
       if (i > 0)
         sb.append(",");
-      String column = sort.getColumn(i);
       sb.append(column);
-      if (sort.getDirection(i) == sort.SORT_ASCENDING)
+      if (sort.getDirection(i) == SortOrder.SORT_ASCENDING)
         sb.append(" ASC");
       else
         sb.append(" DESC");
@@ -706,6 +731,13 @@ public class RepositoryHistoryManager extends 
org.apache.manifoldcf.core.databas
     }
   }
 
+  @Deprecated
+  protected void addOrdering(StringBuilder sb, String[] completeFieldList, 
SortOrder sort)
+    throws ManifoldCFException
+  {
+    addOrdering(sb, completeFieldList, completeFieldList, sort);
+  }
+
   /** Add limit and offset.
   */
   protected void addLimits(StringBuilder sb, int startRow, int maxRowCount)
diff --git 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
index e3f5e74d2..8ef6ac0b5 100644
--- 
a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
+++ 
b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
@@ -2678,6 +2678,82 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
     docStatus.put("hopcountexceeded",new 
Integer(IJobManager.DOCSTATUS_HOPCOUNTEXCEEDED));
   }
 
+  protected final static Set<String> queueDocumentSortColumns;
+  static
+  {
+    queueDocumentSortColumns = new HashSet<String>();
+    queueDocumentSortColumns.add("identifier");
+    queueDocumentSortColumns.add("job");
+    queueDocumentSortColumns.add("state");
+    queueDocumentSortColumns.add("status");
+    queueDocumentSortColumns.add("scheduled");
+    queueDocumentSortColumns.add("action");
+    queueDocumentSortColumns.add("retrycount");
+    queueDocumentSortColumns.add("retrylimit");
+    queueDocumentSortColumns.add("id");
+  }
+
+  protected final static Set<String> queueStatusSortColumns;
+  static
+  {
+    queueStatusSortColumns = new HashSet<String>();
+    queueStatusSortColumns.add("idbucket");
+    queueStatusSortColumns.add("inactive");
+    queueStatusSortColumns.add("processing");
+    queueStatusSortColumns.add("expiring");
+    queueStatusSortColumns.add("deleting");
+    queueStatusSortColumns.add("processready");
+    queueStatusSortColumns.add("expireready");
+    queueStatusSortColumns.add("processwaiting");
+    queueStatusSortColumns.add("expirewaiting");
+    queueStatusSortColumns.add("waitingforever");
+    queueStatusSortColumns.add("hopcountexceeded");
+  }
+
+  protected final static Set<String> historySimpleSortColumns;
+  static
+  {
+    historySimpleSortColumns = new HashSet<String>();
+    historySimpleSortColumns.add("starttime");
+    historySimpleSortColumns.add("resultcode");
+    historySimpleSortColumns.add("resultdesc");
+    historySimpleSortColumns.add("identifier");
+    historySimpleSortColumns.add("activity");
+    historySimpleSortColumns.add("bytes");
+    historySimpleSortColumns.add("elapsedtime");
+    historySimpleSortColumns.add("id");
+    historySimpleSortColumns.add("entityid");
+  }
+
+  protected final static Set<String> historyMaxActivitySortColumns;
+  static
+  {
+    historyMaxActivitySortColumns = new HashSet<String>();
+    historyMaxActivitySortColumns.add("starttime");
+    historyMaxActivitySortColumns.add("endtime");
+    historyMaxActivitySortColumns.add("activitycount");
+    historyMaxActivitySortColumns.add("idbucket");
+  }
+
+  protected final static Set<String> historyMaxBandwidthSortColumns;
+  static
+  {
+    historyMaxBandwidthSortColumns = new HashSet<String>();
+    historyMaxBandwidthSortColumns.add("starttime");
+    historyMaxBandwidthSortColumns.add("endtime");
+    historyMaxBandwidthSortColumns.add("bytecount");
+    historyMaxBandwidthSortColumns.add("idbucket");
+  }
+
+  protected final static Set<String> historyResultSortColumns;
+  static
+  {
+    historyResultSortColumns = new HashSet<String>();
+    historyResultSortColumns.add("idbucket");
+    historyResultSortColumns.add("resultcodebucket");
+    historyResultSortColumns.add("eventcount");
+  }
+
   /** Queue reports */
   protected static int apiReadRepositoryConnectionQueue(IThreadContext tc, 
Configuration output,
     String connectionName, Map<String,List<String>> queryParameters, 
IAuthorizer authorizer) throws ManifoldCFException
@@ -2768,6 +2844,23 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
       
     StatusFilterCriteria filterCriteria = new 
StatusFilterCriteria(jobs,now,idMatch,matchStates,matchStatuses);
       
+    List<String> reportTypeList = queryParameters.get("report");
+    String reportType;
+    if (reportTypeList == null || reportTypeList.size() == 0)
+      reportType = "document";
+    else if (reportTypeList.size() > 1)
+      throw new ManifoldCFException("Multiple report types specified.");
+    else
+      reportType = reportTypeList.get(0);
+
+    Set<String> validColumns;
+    if (reportType.equals("document"))
+      validColumns = queueDocumentSortColumns;
+    else if (reportType.equals("status"))
+      validColumns = queueStatusSortColumns;
+    else
+      throw new ManifoldCFException("Unknown report type '"+reportType+"'.");
+
     // Look for sort order parameters...
     SortOrder sortOrder = new SortOrder();
     List<String> sortColumnsList = queryParameters.get("sortcolumn");
@@ -2779,6 +2872,8 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
       for (int i = 0; i < sortColumnsList.size(); i++)
       {
         String column = sortColumnsList.get(i);
+        if (!validColumns.contains(column.toLowerCase(Locale.ROOT)))
+          throw new ManifoldCFException("Unknown sortcolumn '"+column+"' for 
report '"+reportType+"'.");
         String dir = sortColumnsDirList.get(i);
         int dirInt;
         if (dir.equals("ascending"))
@@ -2810,15 +2905,6 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
     else
       rowCount = new Integer(rowCountList.get(0)).intValue();
 
-    List<String> reportTypeList = queryParameters.get("report");
-    String reportType;
-    if (reportTypeList == null || reportTypeList.size() == 0)
-      reportType = "document";
-    else if (reportTypeList.size() > 1)
-      throw new ManifoldCFException("Multiple report types specified.");
-    else
-      reportType = reportTypeList.get(0);
-
     IJobManager jobManager = JobManagerFactory.make(tc);
       
     IResultSet result;
@@ -2996,6 +3082,27 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
     // Filter criteria
     FilterCriteria filterCriteria = new 
FilterCriteria(activities,startTime,endTime,entityMatch,resultCodeMatch);
       
+    List<String> reportTypeList = queryParameters.get("report");
+    String reportType;
+    if (reportTypeList == null || reportTypeList.size() == 0)
+      reportType = "simple";
+    else if (reportTypeList.size() > 1)
+      throw new ManifoldCFException("Multiple report types specified.");
+    else
+      reportType = reportTypeList.get(0);
+
+    Set<String> validColumns;
+    if (reportType.equals("simple"))
+      validColumns = historySimpleSortColumns;
+    else if (reportType.equals("maxactivity"))
+      validColumns = historyMaxActivitySortColumns;
+    else if (reportType.equals("maxbandwidth"))
+      validColumns = historyMaxBandwidthSortColumns;
+    else if (reportType.equals("result"))
+      validColumns = historyResultSortColumns;
+    else
+      throw new ManifoldCFException("Unknown report type '"+reportType+"'.");
+
     // Look for sort order parameters...
     SortOrder sortOrder = new SortOrder();
     List<String> sortColumnsList = queryParameters.get("sortcolumn");
@@ -3007,6 +3114,8 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
       for (int i = 0; i < sortColumnsList.size(); i++)
       {
         String column = sortColumnsList.get(i);
+        if (!validColumns.contains(column.toLowerCase(Locale.ROOT)))
+          throw new ManifoldCFException("Unknown sortcolumn '"+column+"' for 
report '"+reportType+"'.");
         String dir = sortColumnsDirList.get(i);
         int dirInt;
         if (dir.equals("ascending"))
@@ -3038,15 +3147,6 @@ public class ManifoldCF extends 
org.apache.manifoldcf.agents.system.ManifoldCF
     else
       rowCount = new Integer(rowCountList.get(0)).intValue();
 
-    List<String> reportTypeList = queryParameters.get("report");
-    String reportType;
-    if (reportTypeList == null || reportTypeList.size() == 0)
-      reportType = "simple";
-    else if (reportTypeList.size() > 1)
-      throw new ManifoldCFException("Multiple report types specified.");
-    else
-      reportType = reportTypeList.get(0);
-
     IRepositoryConnectionManager connectionManager = 
RepositoryConnectionManagerFactory.make(tc);
       
     IResultSet result;
diff --git 
a/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SanityHSQLDBTest.java
 
b/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SanityHSQLDBTest.java
index ac73eaa24..797a634fd 100644
--- 
a/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SanityHSQLDBTest.java
+++ 
b/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SanityHSQLDBTest.java
@@ -25,6 +25,8 @@ import org.apache.manifoldcf.agents.system.ManifoldCF;
 import java.io.*;
 import java.util.*;
 import org.junit.*;
+import org.apache.manifoldcf.crawler.interfaces.*;
+import static org.junit.Assert.*;
 
 /** This is a very basic sanity check */
 public class SanityHSQLDBTest extends BaseHSQLDB
@@ -36,6 +38,63 @@ public class SanityHSQLDBTest extends BaseHSQLDB
   {
     // If we get this far, it must mean that the setup was successful, which 
is all that I'm shooting for in this test.
   }
-  
+
+  @Test
+  public void testJobManagerDocumentStatusSortValidation()
+    throws Exception
+  {
+    IThreadContext tc = ThreadContextFactory.make();
+    IJobManager jobManager = JobManagerFactory.make(tc);
+    StatusFilterCriteria filterCriteria = new StatusFilterCriteria(new 
Long[0], System.currentTimeMillis(), null, new int[0], new int[0]);
+
+    // Valid sort columns
+    SortOrder validSort = new SortOrder();
+    validSort.addCriteria("job", SortOrder.SORT_ASCENDING);
+    validSort.addCriteria("status", SortOrder.SORT_DESCENDING);
+    IResultSet set = jobManager.genDocumentStatus("testconn", filterCriteria, 
validSort, 0, 20);
+    assertNotNull(set);
+
+    // Invalid sort column (valid identifier syntax, but not allowed for 
document status)
+    SortOrder invalidSort = new SortOrder();
+    invalidSort.addCriteria("nonexistent_column", SortOrder.SORT_ASCENDING);
+    try
+    {
+      jobManager.genDocumentStatus("testconn", filterCriteria, invalidSort, 0, 
20);
+      fail("Expected ManifoldCFException for invalid sort column in 
genDocumentStatus");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown or invalid sort column: 
'nonexistent_column'"));
+    }
+  }
+
+  @Test
+  public void testHistoryManagerSortValidation()
+    throws Exception
+  {
+    IThreadContext tc = ThreadContextFactory.make();
+    IRepositoryConnectionManager connManager = 
RepositoryConnectionManagerFactory.make(tc);
+    FilterCriteria criteria = new FilterCriteria(new String[0], null, null, 
null, null);
+
+    // Valid sort columns (including entityid alias)
+    SortOrder validSort = new SortOrder();
+    validSort.addCriteria("activity", SortOrder.SORT_ASCENDING);
+    validSort.addCriteria("entityid", SortOrder.SORT_DESCENDING);
+    IResultSet set = connManager.genHistorySimple("testconn", criteria, 
validSort, 0, 20);
+    assertNotNull(set);
+
+    // Invalid sort column
+    SortOrder invalidSort = new SortOrder();
+    invalidSort.addCriteria("unknown_field", SortOrder.SORT_ASCENDING);
+    try
+    {
+      connManager.genHistorySimple("testconn", criteria, invalidSort, 0, 20);
+      fail("Expected ManifoldCFException for invalid sort column in 
genHistorySimple");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown or invalid sort column: 
'unknown_field'"));
+    }
+  }
 
 }
diff --git 
a/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SortOrderTest.java
 
b/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SortOrderTest.java
new file mode 100644
index 000000000..2cb727e06
--- /dev/null
+++ 
b/framework/pull-agent/src/test/java/org/apache/manifoldcf/crawler/tests/SortOrderTest.java
@@ -0,0 +1,264 @@
+/* $Id$ */
+
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package org.apache.manifoldcf.crawler.tests;
+
+import java.util.*;
+import org.apache.manifoldcf.core.interfaces.*;
+import org.apache.manifoldcf.crawler.interfaces.*;
+import org.apache.manifoldcf.crawler.jobs.JobManager;
+import org.apache.manifoldcf.crawler.repository.RepositoryHistoryManager;
+import org.apache.manifoldcf.crawler.system.ManifoldCF;
+import org.junit.*;
+import static org.junit.Assert.*;
+
+public class SortOrderTest
+{
+  @Test
+  public void testSortOrderValid()
+    throws Exception
+  {
+    SortOrder so = new SortOrder();
+    so.addCriteria("identifier", SortOrder.SORT_ASCENDING);
+    so.addCriteria("job", SortOrder.SORT_DESCENDING);
+    assertEquals(2, so.getCount());
+    assertEquals("job", so.getColumn(0));
+    assertEquals(SortOrder.SORT_DESCENDING, so.getDirection(0));
+    assertEquals("identifier", so.getColumn(1));
+    assertEquals(SortOrder.SORT_ASCENDING, so.getDirection(1));
+
+    String rep = so.toString();
+    SortOrder deserialized = new SortOrder(rep);
+    assertEquals(2, deserialized.getCount());
+    assertEquals("job", deserialized.getColumn(0));
+    assertEquals(SortOrder.SORT_DESCENDING, deserialized.getDirection(0));
+    assertEquals("identifier", deserialized.getColumn(1));
+    assertEquals(SortOrder.SORT_ASCENDING, deserialized.getDirection(1));
+  }
+
+  @Test
+  public void testSortOrderClickColumn()
+  {
+    SortOrder so = new SortOrder();
+    so.clickColumn("starttime");
+    assertEquals(1, so.getCount());
+    assertEquals("starttime", so.getColumn(0));
+    assertEquals(SortOrder.SORT_ASCENDING, so.getDirection(0));
+
+    // Clicking again should toggle direction
+    so.clickColumn("starttime");
+    assertEquals(1, so.getCount());
+    assertEquals("starttime", so.getColumn(0));
+    assertEquals(SortOrder.SORT_DESCENDING, so.getDirection(0));
+
+    // Clicking an invalid column should be ignored
+    so.clickColumn("invalid; DROP TABLE repojobs;");
+    assertEquals(1, so.getCount());
+    assertEquals("starttime", so.getColumn(0));
+  }
+
+  @Test
+  public void testSortOrderInjectionRejection()
+  {
+    SortOrder so = new SortOrder();
+    try
+    {
+      so.addCriteria("job; DROP TABLE repojobs;", SortOrder.SORT_ASCENDING);
+      fail("Expected IllegalArgumentException for SQL injection in column 
name");
+    }
+    catch (IllegalArgumentException e)
+    {
+      // Expected
+    }
+
+    try
+    {
+      so.addCriteria("(CASE WHEN 1=1 THEN id ELSE starttime END)", 
SortOrder.SORT_ASCENDING);
+      fail("Expected IllegalArgumentException for expression in column name");
+    }
+    catch (IllegalArgumentException e)
+    {
+      // Expected
+    }
+
+    try
+    {
+      so.addCriteria("starttime DESC--", SortOrder.SORT_ASCENDING);
+      fail("Expected IllegalArgumentException for comment syntax in column 
name");
+    }
+    catch (IllegalArgumentException e)
+    {
+      // Expected
+    }
+
+    try
+    {
+      new SortOrder("1:+job; DROP TABLE repojobs..");
+      fail("Expected ManifoldCFException for SQL injection in serialized 
SortOrder");
+    }
+    catch (ManifoldCFException e)
+    {
+      // Expected
+    }
+  }
+
+  @Test
+  public void testApiQueueSortColumnValidation()
+    throws Exception
+  {
+    IThreadContext tc = ThreadContextFactory.make();
+    IAuthorizer authorizer = new IAuthorizer() {
+      @Override
+      public boolean checkAllowed(IThreadContext tc, int capability) {
+        return true;
+      }
+    };
+
+    // Valid queue document sort column
+    Map<String,List<String>> params = new HashMap<String,List<String>>();
+    params.put("report", Collections.singletonList("document"));
+    params.put("sortcolumn", Arrays.asList("job", "status"));
+    params.put("sortcolumn_direction", Arrays.asList("ascending", 
"descending"));
+
+    // Invalid queue document sort column
+    Map<String,List<String>> badParams = new HashMap<String,List<String>>();
+    badParams.put("report", Collections.singletonList("document"));
+    badParams.put("sortcolumn", 
Collections.singletonList("nonexistent_column"));
+    badParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionqueue/TestConn", badParams, authorizer);
+      fail("Expected ManifoldCFException for invalid sort column in queue 
document report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn 
'nonexistent_column'"));
+    }
+
+    // SQL injection payload in queue document sort column
+    Map<String,List<String>> sqlInjParams = new HashMap<String,List<String>>();
+    sqlInjParams.put("report", Collections.singletonList("document"));
+    sqlInjParams.put("sortcolumn", Collections.singletonList("(CASE WHEN (1=1) 
THEN identifier ELSE scheduled END)"));
+    sqlInjParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionqueue/TestConn", sqlInjParams, authorizer);
+      fail("Expected ManifoldCFException for SQL injection payload in queue 
document report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn"));
+    }
+
+    // Invalid queue status sort column
+    Map<String,List<String>> badStatusParams = new 
HashMap<String,List<String>>();
+    badStatusParams.put("report", Collections.singletonList("status"));
+    badStatusParams.put("sortcolumn", Collections.singletonList("job"));
+    badStatusParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionqueue/TestConn", badStatusParams, authorizer);
+      fail("Expected ManifoldCFException for 'job' sort column in queue status 
report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn 'job' for report 
'status'"));
+    }
+  }
+
+  @Test
+  public void testApiHistorySortColumnValidation()
+    throws Exception
+  {
+    IThreadContext tc = ThreadContextFactory.make();
+    IAuthorizer authorizer = new IAuthorizer() {
+      @Override
+      public boolean checkAllowed(IThreadContext tc, int capability) {
+        return true;
+      }
+    };
+
+    // Invalid history simple sort column
+    Map<String,List<String>> badSimpleParams = new 
HashMap<String,List<String>>();
+    badSimpleParams.put("report", Collections.singletonList("simple"));
+    badSimpleParams.put("sortcolumn", Collections.singletonList("bad_column"));
+    badSimpleParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionhistory/TestConn", badSimpleParams, authorizer);
+      fail("Expected ManifoldCFException for invalid sort column in history 
simple report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn 'bad_column' for 
report 'simple'"));
+    }
+
+    // SQL injection payload in history simple sort column
+    Map<String,List<String>> sqlInjParams = new HashMap<String,List<String>>();
+    sqlInjParams.put("report", Collections.singletonList("simple"));
+    sqlInjParams.put("sortcolumn", Collections.singletonList("starttime; 
pg_sleep(5);--"));
+    sqlInjParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionhistory/TestConn", sqlInjParams, authorizer);
+      fail("Expected ManifoldCFException for SQL injection payload in history 
simple report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn"));
+    }
+
+    // Invalid maxactivity sort column
+    Map<String,List<String>> badMaxActParams = new 
HashMap<String,List<String>>();
+    badMaxActParams.put("report", Collections.singletonList("maxactivity"));
+    badMaxActParams.put("sortcolumn", Collections.singletonList("bytes"));
+    badMaxActParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionhistory/TestConn", badMaxActParams, authorizer);
+      fail("Expected ManifoldCFException for 'bytes' sort column in 
maxactivity report");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown sortcolumn 'bytes' for 
report 'maxactivity'"));
+    }
+
+    // Invalid report type
+    Map<String,List<String>> badReportParams = new 
HashMap<String,List<String>>();
+    badReportParams.put("report", 
Collections.singletonList("nonexistent_report"));
+    badReportParams.put("sortcolumn", Collections.singletonList("starttime"));
+    badReportParams.put("sortcolumn_direction", 
Collections.singletonList("ascending"));
+
+    try
+    {
+      ManifoldCF.executeReadCommand(tc, new Configuration(), 
"repositoryconnectionhistory/TestConn", badReportParams, authorizer);
+      fail("Expected ManifoldCFException for unknown report type");
+    }
+    catch (ManifoldCFException e)
+    {
+      assertTrue(e.getMessage().contains("Unknown report type 
'nonexistent_report'"));
+    }
+  }
+
+}

Reply via email to