Moderate severityOSV Advisory· Published Jan 14, 2026· Updated Jan 15, 2026
Apache Camel Neo4j: Cypher injection vulnerability in Camel-Neo4j component
CVE-2025-66169
Description
Cypher Injection vulnerability in Apache Camel camel-neo4j component.
This issue affects Apache Camel: from 4.10.0 before 4.10.8, from 4.14.0 before 4.14.3, from 4.15.0 before 4.17.0
Users are recommended to upgrade to version 4.10.8 for 4.10.x LTS and 4.14.3 for 4.14.x LTS and 4.17.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.camel:camel-neo4jMaven | >= 4.10.0, < 4.10.8 | 4.10.8 |
org.apache.camel:camel-neo4jMaven | >= 4.14.0, < 4.14.3 | 4.14.3 |
org.apache.camel:camel-neo4jMaven | >= 4.15.0, < 4.17.0 | 4.17.0 |
Affected products
1Patches
3723e2cd98ce4CAMEL-22719 - camel-neo4j - Improve detection of message body (#20037)
3 files changed · +124 −29
components/camel-ai/camel-neo4j/pom.xml+4 −0 modified@@ -47,6 +47,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-jackson</artifactId> + </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId>
components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java+111 −20 modified@@ -21,6 +21,8 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.Message; @@ -50,6 +52,10 @@ public class Neo4jProducer extends DefaultProducer { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new TypeReference<>() { + }; + private Driver driver; public Neo4jProducer(Neo4jEndpoint endpoint) { @@ -104,15 +110,24 @@ private void createNode(Exchange exchange) throws InvalidPayloadException { final String databaseName = getEndpoint().getName(); - var query = ""; - Map<String, Object> properties = null; + // Always use parameterized queries to prevent Cypher injection + var query = String.format("CREATE (%s:%s $props)", alias, label); + Map<String, Object> properties; if (body instanceof String) { - // Case we get the object in a Json format - query = String.format("CREATE (%s:%s %s)", alias, label, body); + try { + // Convert JSON string to Map for parameterized query + Map<String, Object> bodyMap = OBJECT_MAPPER.readValue((String) body, MAP_TYPE_REF); + properties = Map.of("props", bodyMap); + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.CREATE_NODE, + new IllegalArgumentException("Failed to parse body as JSON: " + body, e))); + return; + } } else { - // body should be a list of properties - query = String.format("CREATE (%s:%s $props)", alias, label); + // body should be a Map or similar object properties = Map.of("props", body); } @@ -126,17 +141,55 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); - var query = String.format("MATCH (%s:%s %s) RETURN %s", alias, label, matchQuery, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Search all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s RETURN %s", + alias, label, whereClause.toString(), alias); + } else { + // Empty map, match all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + RETRIEVE_NODES, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - queryRetriveNodes(exchange, databaseName, null, query, RETRIEVE_NODES); + queryRetriveNodes(exchange, databaseName, queryParams, query, RETRIEVE_NODES); } private void retrieveNodesWithCypherQuery(Exchange exchange) throws NoSuchHeaderException { @@ -184,19 +237,57 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); final String detached = getEndpoint().getConfiguration().isDetachRelationship() ? "DETACH" : ""; - var query = String.format("MATCH (%s:%s %s) %s DELETE %s", alias, label, matchQuery, detached, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s %s DELETE %s", + alias, label, whereClause.toString(), detached, alias); + } else { + // Empty map, delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.DELETE_NODE, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - executeWriteQuery(exchange, query, null, databaseName, Neo4Operation.DELETE_NODE); + executeWriteQuery(exchange, query, queryParams, databaseName, Neo4Operation.DELETE_NODE); } private void createVectorIndex(Exchange exchange) {
components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java+9 −9 modified@@ -41,8 +41,8 @@ public class Neo4jNodeIT extends Neo4jTestSupport { @Order(0) void createNodeWithJsonObject() { - var body = "{name: 'Alice', email: 'alice@example.com', age: 30}"; - var expectedCypherQuery = "CREATE (u1:User {name: 'Alice', email: 'alice@example.com', age: 30})"; + var body = "{\"name\": \"Alice\", \"email\": \"alice@example.com\", \"age\": 30}"; + var expectedCypherQuery = "CREATE (u1:User $props)"; Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u1&label=User") .withBodyAs(body, String.class) @@ -141,7 +141,7 @@ void testCreateMultipleNodesAndRelationshipWithCypherQuery() { void testRetrieveNode() { Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -193,7 +193,7 @@ void testDeleteNode() { // delete node Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -215,7 +215,7 @@ void testDeleteNode() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -233,7 +233,7 @@ void testDeleteNodeWithExistingRelationship() { // try to delete user named Diana and this should fail as Diana has a relationship with Ethan Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); @@ -245,7 +245,7 @@ void testDeleteNodeWithExistingRelationship() { // delete the Diana by detaching its relationship with Ethan - detachRelationship=true result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User&detachRelationship=true") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); assertNull("No exception anymore when deleting relationship at same time", result.getException()); @@ -269,7 +269,7 @@ void testDeleteNodeWithExistingRelationship() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); @@ -311,7 +311,7 @@ void testDeleteNodeWithCypherQuery() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Bob'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Bob\"}") .request(Exchange.class); assertNotNull(result);
e46c4c0ef542CAMEL-22719 - camel-neo4j - Improve detection of message body (#20036)
3 files changed · +124 −29
components/camel-ai/camel-neo4j/pom.xml+4 −0 modified@@ -45,6 +45,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-jackson</artifactId> + </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId>
components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java+111 −20 modified@@ -21,6 +21,8 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.Message; @@ -50,6 +52,10 @@ public class Neo4jProducer extends DefaultProducer { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new TypeReference<>() { + }; + private Driver driver; public Neo4jProducer(Neo4jEndpoint endpoint) { @@ -104,15 +110,24 @@ private void createNode(Exchange exchange) throws InvalidPayloadException { final String databaseName = getEndpoint().getName(); - var query = ""; - Map<String, Object> properties = null; + // Always use parameterized queries to prevent Cypher injection + var query = String.format("CREATE (%s:%s $props)", alias, label); + Map<String, Object> properties; if (body instanceof String) { - // Case we get the object in a Json format - query = String.format("CREATE (%s:%s %s)", alias, label, body); + try { + // Convert JSON string to Map for parameterized query + Map<String, Object> bodyMap = OBJECT_MAPPER.readValue((String) body, MAP_TYPE_REF); + properties = Map.of("props", bodyMap); + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.CREATE_NODE, + new IllegalArgumentException("Failed to parse body as JSON: " + body, e))); + return; + } } else { - // body should be a list of properties - query = String.format("CREATE (%s:%s $props)", alias, label); + // body should be a Map or similar object properties = Map.of("props", body); } @@ -126,17 +141,55 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); - var query = String.format("MATCH (%s:%s %s) RETURN %s", alias, label, matchQuery, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Search all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s RETURN %s", + alias, label, whereClause.toString(), alias); + } else { + // Empty map, match all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + RETRIEVE_NODES, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - queryRetriveNodes(exchange, databaseName, null, query, RETRIEVE_NODES); + queryRetriveNodes(exchange, databaseName, queryParams, query, RETRIEVE_NODES); } private void retrieveNodesWithCypherQuery(Exchange exchange) throws NoSuchHeaderException { @@ -184,19 +237,57 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); final String detached = getEndpoint().getConfiguration().isDetachRelationship() ? "DETACH" : ""; - var query = String.format("MATCH (%s:%s %s) %s DELETE %s", alias, label, matchQuery, detached, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s %s DELETE %s", + alias, label, whereClause.toString(), detached, alias); + } else { + // Empty map, delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.DELETE_NODE, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - executeWriteQuery(exchange, query, null, databaseName, Neo4Operation.DELETE_NODE); + executeWriteQuery(exchange, query, queryParams, databaseName, Neo4Operation.DELETE_NODE); } private void createVectorIndex(Exchange exchange) {
components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java+9 −9 modified@@ -41,8 +41,8 @@ public class Neo4jNodeIT extends Neo4jTestSupport { @Order(0) void createNodeWithJsonObject() { - var body = "{name: 'Alice', email: 'alice@example.com', age: 30}"; - var expectedCypherQuery = "CREATE (u1:User {name: 'Alice', email: 'alice@example.com', age: 30})"; + var body = "{\"name\": \"Alice\", \"email\": \"alice@example.com\", \"age\": 30}"; + var expectedCypherQuery = "CREATE (u1:User $props)"; Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u1&label=User") .withBodyAs(body, String.class) @@ -141,7 +141,7 @@ void testCreateMultipleNodesAndRelationshipWithCypherQuery() { void testRetrieveNode() { Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -193,7 +193,7 @@ void testDeleteNode() { // delete node Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -215,7 +215,7 @@ void testDeleteNode() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); assertNotNull(result); @@ -233,7 +233,7 @@ void testDeleteNodeWithExistingRelationship() { // try to delete user named Diana and this should fail as Diana has a relationship with Ethan Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); @@ -245,7 +245,7 @@ void testDeleteNodeWithExistingRelationship() { // delete the Diana by detaching its relationship with Ethan - detachRelationship=true result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User&detachRelationship=true") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); assertNull("No exception anymore when deleting relationship at same time", result.getException()); @@ -269,7 +269,7 @@ void testDeleteNodeWithExistingRelationship() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); assertNotNull(result); @@ -311,7 +311,7 @@ void testDeleteNodeWithCypherQuery() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jConstants.Headers.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{name: 'Bob'}") + .withHeader(Neo4jConstants.Headers.MATCH_PROPERTIES, "{\"name\": \"Bob\"}") .request(Exchange.class); assertNotNull(result);
66715d3feb4bCAMEL-22719 - camel-neo4j - Improve detection of message body (#20035)
3 files changed · +124 −29
components/camel-ai/camel-neo4j/pom.xml+4 −0 modified@@ -45,6 +45,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-jackson</artifactId> + </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId>
components/camel-ai/camel-neo4j/src/main/java/org/apache/camel/component/neo4j/Neo4jProducer.java+111 −20 modified@@ -21,6 +21,8 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.Message; @@ -50,6 +52,10 @@ public class Neo4jProducer extends DefaultProducer { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new TypeReference<>() { + }; + private Driver driver; public Neo4jProducer(Neo4jEndpoint endpoint) { @@ -104,15 +110,24 @@ private void createNode(Exchange exchange) throws InvalidPayloadException { final String databaseName = getEndpoint().getName(); - var query = ""; - Map<String, Object> properties = null; + // Always use parameterized queries to prevent Cypher injection + var query = String.format("CREATE (%s:%s $props)", alias, label); + Map<String, Object> properties; if (body instanceof String) { - // Case we get the object in a Json format - query = String.format("CREATE (%s:%s %s)", alias, label, body); + try { + // Convert JSON string to Map for parameterized query + Map<String, Object> bodyMap = OBJECT_MAPPER.readValue((String) body, MAP_TYPE_REF); + properties = Map.of("props", bodyMap); + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.CREATE_NODE, + new IllegalArgumentException("Failed to parse body as JSON: " + body, e))); + return; + } } else { - // body should be a list of properties - query = String.format("CREATE (%s:%s $props)", alias, label); + // body should be a Map or similar object properties = Map.of("props", body); } @@ -126,17 +141,55 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); - var query = String.format("MATCH (%s:%s %s) RETURN %s", alias, label, matchQuery, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Search all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s RETURN %s", + alias, label, whereClause.toString(), alias); + } else { + // Empty map, match all nodes + query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + RETRIEVE_NODES, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - queryRetriveNodes(exchange, databaseName, null, query, RETRIEVE_NODES); + queryRetriveNodes(exchange, databaseName, queryParams, query, RETRIEVE_NODES); } private void retrieveNodesWithCypherQuery(Exchange exchange) throws NoSuchHeaderException { @@ -184,19 +237,57 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException { final String alias = getEndpoint().getConfiguration().getAlias(); ObjectHelper.notNull(alias, "alias"); - String matchQuery = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); - // in this case we search all nodes - if (matchQuery == null) { - matchQuery = ""; - } + String matchProperties = exchange.getMessage().getHeader(MATCH_PROPERTIES, String.class); final String databaseName = getEndpoint().getName(); final String detached = getEndpoint().getConfiguration().isDetachRelationship() ? "DETACH" : ""; - var query = String.format("MATCH (%s:%s %s) %s DELETE %s", alias, label, matchQuery, detached, alias); + String query; + Map<String, Object> queryParams = null; + + if (matchProperties == null || matchProperties.isEmpty()) { + // Delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } else { + try { + // Convert JSON string to Map and build WHERE clause with parameters + Map<String, Object> matchMap = OBJECT_MAPPER.readValue(matchProperties, MAP_TYPE_REF); + + if (!matchMap.isEmpty()) { + StringBuilder whereClause = new StringBuilder(); + queryParams = new java.util.HashMap<>(); + int paramIndex = 0; + + for (Map.Entry<String, Object> entry : matchMap.entrySet()) { + if (paramIndex > 0) { + whereClause.append(" AND "); + } + String paramName = "param" + paramIndex; + whereClause.append(alias).append(".").append(entry.getKey()) + .append(" = $").append(paramName); + queryParams.put(paramName, entry.getValue()); + paramIndex++; + } + + query = String.format("MATCH (%s:%s) WHERE %s %s DELETE %s", + alias, label, whereClause.toString(), detached, alias); + } else { + // Empty map, delete all nodes of this label + query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias); + } + } catch (Exception e) { + exchange.setException( + new Neo4jOperationException( + Neo4Operation.DELETE_NODE, + new IllegalArgumentException( + "Failed to parse MATCH_PROPERTIES as JSON: " + matchProperties, + e))); + return; + } + } - executeWriteQuery(exchange, query, null, databaseName, Neo4Operation.DELETE_NODE); + executeWriteQuery(exchange, query, queryParams, databaseName, Neo4Operation.DELETE_NODE); } private void createVectorIndex(Exchange exchange) {
components/camel-ai/camel-neo4j/src/test/java/org/apache/camel/component/neo4j/it/Neo4jNodeIT.java+9 −9 modified@@ -42,8 +42,8 @@ public class Neo4jNodeIT extends Neo4jTestSupport { @Order(0) void createNodeWithJsonObject() { - var body = "{name: 'Alice', email: 'alice@example.com', age: 30}"; - var expectedCypherQuery = "CREATE (u1:User {name: 'Alice', email: 'alice@example.com', age: 30})"; + var body = "{\"name\": \"Alice\", \"email\": \"alice@example.com\", \"age\": 30}"; + var expectedCypherQuery = "CREATE (u1:User $props)"; Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u1&label=User") .withBodyAs(body, String.class) @@ -144,7 +144,7 @@ void testCreateMultipleNodesAndRelationshipWithCypherQuery() { void testRetrieveNode() { Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -196,7 +196,7 @@ void testDeleteNode() { // delete node Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -218,7 +218,7 @@ void testDeleteNode() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Alice'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Alice\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -236,7 +236,7 @@ void testDeleteNodeWithExistingRelationship() { // try to delete user named Diana and this should fail as Diana has a relationship with Ethan Exchange result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -247,7 +247,7 @@ void testDeleteNodeWithExistingRelationship() { // delete the Diana by detaching its relationship with Ethan - detachRelationship=true result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User&detachRelationship=true") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.DELETE_NODE) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); Assertions.assertNull(result.getException(), "No exception anymore when deleting relationship at same time"); @@ -270,7 +270,7 @@ void testDeleteNodeWithExistingRelationship() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Diana'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Diana\"}") .request(Exchange.class); Assertions.assertNotNull(result); @@ -312,7 +312,7 @@ void testDeleteNodeWithCypherQuery() { result = fluentTemplate.to("neo4j:neo4j?alias=u&label=User") .withHeader(Neo4jHeaders.OPERATION, Neo4Operation.RETRIEVE_NODES) - .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{name: 'Bob'}") + .withHeader(Neo4jHeaders.MATCH_PROPERTIES, "{\"name\": \"Bob\"}") .request(Exchange.class); Assertions.assertNotNull(result);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- camel.apache.org/security/CVE-2025-66169.htmlghsavendor-advisoryWEB
- github.com/advisories/GHSA-4jrw-64vr-7g8mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66169ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/01/13/5ghsaWEB
- github.com/apache/camel/commit/66715d3feb4ba15df30cffe437e45efeedfba10dghsaWEB
- github.com/apache/camel/commit/723e2cd98ce4b4ceb1dd38837bc113fca0cef170ghsaWEB
- github.com/apache/camel/commit/e46c4c0ef542a64dc791253763a8273dfd7fb179ghsaWEB
- github.com/apache/camel/pull/20035ghsaWEB
- github.com/apache/camel/pull/20036ghsaWEB
- github.com/apache/camel/pull/20037ghsaWEB
- issues.apache.org/jira/browse/CAMEL-22719ghsaWEB
News mentions
0No linked articles in our index yet.