This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new bb4176fe87dc CAMEL-23528: validate property names when building
MATCH/DELETE WHERE clause
bb4176fe87dc is described below
commit bb4176fe87dc0cc60a5be37a57b69b8c610c1dd2
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon May 18 10:03:54 2026 +0200
CAMEL-23528: validate property names when building MATCH/DELETE WHERE clause
Validate Neo4j property names used to build the MATCH/DELETE WHERE clause
in Neo4jProducer, rejecting invalid names with a clear error instead of
producing a malformed query. Adds a unit test.
Closes #23258
---
.../camel/component/neo4j/Neo4jProducer.java | 26 ++++++++++++
.../neo4j/Neo4jPropertyNameValidationTest.java | 46 ++++++++++++++++++++++
2 files changed, 72 insertions(+)
diff --git
a/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
b/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
index fbcb313eda92..b829cc6d755d 100644
---
a/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
+++
b/components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java
@@ -20,6 +20,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -57,6 +58,10 @@ public class Neo4jProducer extends DefaultProducer {
private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new
TypeReference<>() {
};
+ // Only property values are passed as bound parameters; the property name
is spliced into the
+ // Cypher query text, so it must be restricted to a safe identifier
pattern.
+ private static final Pattern VALID_PROPERTY_NAME =
Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
+
private Driver driver;
public Neo4jProducer(Neo4jEndpoint endpoint) {
@@ -135,6 +140,19 @@ public class Neo4jProducer extends DefaultProducer {
executeWriteQuery(exchange, query, properties, databaseName,
Neo4Operation.CREATE_NODE);
}
+ /**
+ * Validates a Neo4j property name before it is spliced into a Cypher
query. Property values are passed as bound
+ * parameters, but the property name is inserted into the query text, so
it must be a safe identifier. Names that do
+ * not match are rejected with a clear error instead of producing a
malformed or unintended query.
+ */
+ static void validatePropertyName(String name) {
+ if (name == null || !VALID_PROPERTY_NAME.matcher(name).matches()) {
+ throw new IllegalArgumentException(
+ "Invalid Neo4j property name: '" + name + "'. Property
names must match "
+ +
VALID_PROPERTY_NAME.pattern());
+ }
+ }
+
private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException
{
final String label = getEndpoint().getConfiguration().getLabel();
ObjectHelper.notNull(label, "label");
@@ -166,6 +184,7 @@ public class Neo4jProducer extends DefaultProducer {
if (paramIndex > 0) {
whereClause.append(" AND ");
}
+ validatePropertyName(entry.getKey());
String paramName = "param" + paramIndex;
whereClause.append(alias).append(".").append(entry.getKey())
.append(" = $").append(paramName);
@@ -179,6 +198,9 @@ public class Neo4jProducer extends DefaultProducer {
// Empty map, match all nodes
query = String.format("MATCH (%s:%s) RETURN %s", alias,
label, alias);
}
+ } catch (IllegalArgumentException iae) {
+ exchange.setException(new
Neo4jOperationException(RETRIEVE_NODES, iae));
+ return;
} catch (Exception e) {
exchange.setException(
new Neo4jOperationException(
@@ -264,6 +286,7 @@ public class Neo4jProducer extends DefaultProducer {
if (paramIndex > 0) {
whereClause.append(" AND ");
}
+ validatePropertyName(entry.getKey());
String paramName = "param" + paramIndex;
whereClause.append(alias).append(".").append(entry.getKey())
.append(" = $").append(paramName);
@@ -277,6 +300,9 @@ public class Neo4jProducer extends DefaultProducer {
// Empty map, delete all nodes of this label
query = String.format("MATCH (%s:%s) %s DELETE %s", alias,
label, detached, alias);
}
+ } catch (IllegalArgumentException iae) {
+ exchange.setException(new
Neo4jOperationException(Neo4Operation.DELETE_NODE, iae));
+ return;
} catch (Exception e) {
exchange.setException(
new Neo4jOperationException(
diff --git
a/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/Neo4jPropertyNameValidationTest.java
b/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/Neo4jPropertyNameValidationTest.java
new file mode 100644
index 000000000000..f4dff68e931d
--- /dev/null
+++
b/components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/Neo4jPropertyNameValidationTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.camel.component.neo4j;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class Neo4jPropertyNameValidationTest {
+
+ @Test
+ void acceptsValidPropertyNames() {
+ assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("name"));
+ assertDoesNotThrow(() ->
Neo4jProducer.validatePropertyName("_internal"));
+ assertDoesNotThrow(() ->
Neo4jProducer.validatePropertyName("firstName2"));
+ assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("A_B_1"));
+ }
+
+ @Test
+ void rejectsInvalidPropertyNames() {
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName(null));
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName(""));
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName("first name"));
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName("name-1"));
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName("name.sub"));
+ assertThrows(IllegalArgumentException.class, () ->
Neo4jProducer.validatePropertyName("1name"));
+ // A property name that would otherwise change the structure of the
generated query.
+ assertThrows(IllegalArgumentException.class,
+ () -> Neo4jProducer.validatePropertyName("x) RETURN n UNION
MATCH (m) RETURN m //"));
+ }
+}