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 fbcb313eda928..b829cc6d755d6 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.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_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 @@ private void createNode(Exchange exchange) throws InvalidPayloadException { 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 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { 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 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { // 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 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { 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 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { // 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 0000000000000..f4dff68e931d0 --- /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 //")); + } +}