This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 7652078269dfebbe20cfa38d1d0b7da759d6fa14 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 2f77c4c8b2..f80667d705 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("(beforeValue 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 "(beforeValue 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 de1c2307d7..b4233ab54e 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 @@ -41,6 +41,7 @@ import org.apache.syncope.common.lib.to.AuditEventTO; 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.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; @@ -50,7 +51,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); @@ -95,6 +96,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"). @@ -123,7 +126,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 eb5d63feaf..45d3034a99 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 @@ -28,6 +28,7 @@ import org.apache.syncope.common.lib.to.AuditEventTO; 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.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; @@ -49,7 +50,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); @@ -94,6 +95,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"). @@ -122,7 +125,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()).
