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

ilgrosso pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 947f2964d4d659dea9d17b27ef88373fe7ebd968
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Thu Aug 20 11:39:18 2026 +0200

    Ensuring to escape Audit Events search parameters for LIKE
---
 .../common/dao/AbstractAuditEventDAO.java          | 29 +++++++++++++++++++++-
 .../core/persistence/jpa/dao/JPAAuditEventDAO.java | 24 +++++++-----------
 .../persistence/neo4j/dao/Neo4jAuditEventDAO.java  |  8 ++++--
 .../dao/ElasticsearchAuditEventDAO.java            |  7 ++++--
 .../opensearch/dao/OpenSearchAuditEventDAO.java    |  7 ++++--
 5 files changed, 53 insertions(+), 22 deletions(-)

diff --git 
a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAuditEventDAO.java
 
b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAuditEventDAO.java
index a84dd746fc..497be6380a 100644
--- 
a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAuditEventDAO.java
+++ 
b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAuditEventDAO.java
@@ -20,13 +20,40 @@ package org.apache.syncope.core.persistence.common.dao;
 
 import java.util.List;
 import java.util.stream.Stream;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.OpEvent;
 import org.apache.syncope.core.persistence.api.entity.AuditEvent;
 import org.springframework.data.domain.Sort;
 import org.springframework.util.ReflectionUtils;
 
 public abstract class AbstractAuditEventDAO {
 
-    protected List<Sort.Order> filterOrderBy(
+    protected static void checkEntityKey(final String entityKey) {
+        if (!SyncopeConstants.UUID_PATTERN.matcher(entityKey).matches()) {
+            throw new IllegalArgumentException("Invalid entityKey: " + 
entityKey);
+        }
+    }
+
+    // The free-form value is bound as a query parameter and its LIKE 
metacharacters are escaped, 
+    // so '%'/'_' in the value match literally and there is no injection 
surface.
+    // '#' is used as the LIKE escape char (instead of '\') and applied 
uniformly: a single '\' in
+    // native SQL is mishandled by MySQL/MariaDB and Oracle has no default 
LIKE escape, whereas '#'
+    // works across every supported database.
+    protected static String escapeForLike(final String value) {
+        return value == null ? null : value.replace("#", "##").replace("%", 
"#%").replace("_", "#_");
+    }
+
+    protected static String toOpEvent(
+            final OpEvent.CategoryType type,
+            final String category,
+            final String subcategory,
+            final String op,
+            final OpEvent.Outcome outcome) {
+
+        return OpEvent.toString(type, escapeForLike(category), 
escapeForLike(subcategory), escapeForLike(op), outcome);
+    }
+
+    protected static List<Sort.Order> filterOrderBy(
             final Stream<Sort.Order> orderByClauses,
             final Class<? extends AuditEvent> clazz) {
 
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
index 96d555e394..f03618f27e 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
@@ -54,6 +54,8 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
 
         protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
             if (entityKey != null) {
+                checkEntityKey(entityKey);
+
                 query.append(andIfNeeded()).
                         append("(before_value LIKE 
'%\"key\":\"").append(entityKey).append("\"%' OR ").
                         append("inputs LIKE 
'%\"key\":\"").append(entityKey).append("\"%' OR ").
@@ -63,19 +65,10 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
             return this;
         }
 
-        // Unlike the entityKey predicate above (a constrained UUID, 
concatenated), the free-form username
-        // is bound as a query parameter and its LIKE metacharacters are 
escaped, so '%'/'_' in a username
-        // match literally and there is no injection surface.
-        // '#' is used as the LIKE escape char (instead of '\') and applied 
uniformly: a single '\' in
-        // native SQL is mishandled by MySQL/MariaDB and Oracle has no default 
LIKE escape, whereas '#'
-        // works across every supported database (this DAO is not subclassed 
per-database).
-        protected static String escapeForLike(final String value) {
-            return value.replace("#", "##").replace("%", "#%").replace("_", 
"#_");
-        }
-
         public AuditEventCriteriaBuilder username(final Set<String> username, 
final List<Object> parameters) {
             if (!CollectionUtils.isEmpty(username)) {
-                query.append(andIfNeeded()).append("(").
+                query.append(andIfNeeded()).
+                        append('(').
                         append(username.stream().map(value -> {
                             String pattern = "%\"username\":\"" + 
escapeForLike(value) + "\"%";
                             return "(before_value LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#'"
@@ -83,7 +76,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
                                     + " OR output LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#'"
                                     + " OR throwable LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#')";
                         }).collect(Collectors.joining(" OR "))).
-                        append(")");
+                        append(')');
             }
             return this;
         }
@@ -108,7 +101,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
 
             query.append(andIfNeeded()).
                     append("opEvent LIKE '").
-                    append(OpEvent.toString(type, category, subcategory, op, 
outcome).replace("[]", "[%]")).
+                    append(toOpEvent(type, category, subcategory, op, 
outcome).replace("[]", "[%]")).
                     append("'");
 
             return this;
@@ -220,8 +213,9 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
         List<Sort.Order> orderBy = filterOrderBy(pageable.getSort().stream(), 
JPAAuditEvent.class);
         if (!orderBy.isEmpty()) {
             queryString += " ORDER BY " + orderBy.stream().
-                    map(clause -> ("when".equals(clause.getProperty()) ? 
"event_date" : clause.getProperty())
-                    + ' ' + clause.getDirection().name()).
+                    map(clause -> ("when".equals(clause.getProperty())
+                            ? "event_date"
+                            : clause.getProperty()) + ' ' + 
clause.getDirection().name()).
                     collect(Collectors.joining(","));
         }
 
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
index b976c3a162..26615362c7 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
@@ -51,6 +51,8 @@ public class Neo4jAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEv
 
         protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
             if (entityKey != null) {
+                checkEntityKey(entityKey);
+
                 query.append(andIfNeeded()).
                         append("(n.before =~ 
'.*key.*").append(entityKey).append(".*' OR ").
                         append("n.inputs =~ 
'.*key.*").append(entityKey).append(".*' OR ").
@@ -62,7 +64,9 @@ public class Neo4jAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEv
 
         public AuditEventCriteriaBuilder username(final Set<String> username, 
final Map<String, Object> parameters) {
             if (!CollectionUtils.isEmpty(username)) {
-                parameters.put("usernames", username.stream().map(value -> 
"\"username\":\"" + value + "\"").toList());
+                parameters.put("usernames", username.stream().
+                        map(value -> "\"username\":\"" + escapeForLike(value) 
+ "\"").
+                        toList());
                 query.append(andIfNeeded()).
                         append("ANY(u IN $usernames WHERE n.before CONTAINS u 
OR n.inputs CONTAINS u "
                                 + "OR n.output CONTAINS u OR n.throwable 
CONTAINS u)");
@@ -87,7 +91,7 @@ public class Neo4jAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEv
 
             query.append(andIfNeeded()).
                     append("n.opEvent =~ '").
-                    append(OpEvent.toString(type, category, subcategory, op, 
outcome).
+                    append(toOpEvent(type, category, subcategory, op, outcome).
                             replace("[", "\\[").replace("]", 
"\\]").replace("\\[\\]", "\\[.*\\]")).
                     append("'");
 
diff --git 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
index 4d98447140..05ec70d1eb 100644
--- 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
+++ 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
@@ -44,6 +44,7 @@ import org.apache.syncope.common.lib.types.OpEvent;
 import org.apache.syncope.core.persistence.api.dao.AuditEventDAO;
 import org.apache.syncope.core.persistence.api.entity.AuditEvent;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.persistence.common.dao.AbstractAuditEventDAO;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.ext.elasticsearch.client.ElasticsearchIndexManager;
 import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
@@ -53,7 +54,7 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.util.CollectionUtils;
 
-public class ElasticsearchAuditEventDAO implements AuditEventDAO {
+public class ElasticsearchAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEventDAO {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(AuditEventDAO.class);
 
@@ -98,6 +99,8 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
         List<Query> queries = new ArrayList<>();
 
         if (entityKey != null) {
+            checkEntityKey(entityKey);
+
             queries.add(new Query.Builder().
                     multiMatch(QueryBuilders.multiMatch().
                             fields("before", "inputs", "output", "throwable").
@@ -126,7 +129,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
 
         queries.add(new Query.Builder().regexp(QueryBuilders.regexp().
                 field("opEvent").
-                value(OpEvent.toString(type, category, subcategory, op, 
outcome).
+                value(toOpEvent(type, category, subcategory, op, outcome).
                         replace("[]", "[.*]").
                         replace("[", "\\[").
                         replace("]", "\\]")).build()).
diff --git 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
index f940d25baa..be7263859d 100644
--- 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
+++ 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
@@ -31,6 +31,7 @@ import org.apache.syncope.common.lib.types.OpEvent;
 import org.apache.syncope.core.persistence.api.dao.AuditEventDAO;
 import org.apache.syncope.core.persistence.api.entity.AuditEvent;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.persistence.common.dao.AbstractAuditEventDAO;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.ext.opensearch.client.OpenSearchIndexManager;
 import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
@@ -52,7 +53,7 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.util.CollectionUtils;
 
-public class OpenSearchAuditEventDAO implements AuditEventDAO {
+public class OpenSearchAuditEventDAO extends AbstractAuditEventDAO implements 
AuditEventDAO {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(AuditEventDAO.class);
 
@@ -97,6 +98,8 @@ public class OpenSearchAuditEventDAO implements AuditEventDAO 
{
         List<Query> queries = new ArrayList<>();
 
         if (entityKey != null) {
+            checkEntityKey(entityKey);
+
             queries.add(new Query.Builder().
                     multiMatch(QueryBuilders.multiMatch().
                             fields("before", "inputs", "output", "throwable").
@@ -125,7 +128,7 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
 
         queries.add(new Query.Builder().regexp(QueryBuilders.regexp().
                 field("opEvent").
-                value(OpEvent.toString(type, category, subcategory, op, 
outcome).
+                value(toOpEvent(type, category, subcategory, op, outcome).
                         replace("[]", "[.*]").
                         replace("[", "\\[").
                         replace("]", "\\]")).build()).

Reply via email to