Files
semantica/tests/test_graph_store_methods.py
Mohd KaifandSameer6305 b846ff88d4 security: sanitize Cypher labels/relationship types/property keys (GHSA-482h) (#910)
* security: sanitize Cypher labels/relationship types/property keys (GHSA-482h-hw99-h62p)

Node labels and property keys passed to create_node/create_relationship
were interpolated directly into Cypher strings in the Neptune, Neo4j, and
FalkorDB graph stores. Property values are parameterized, but labels and
keys can't be bound as parameters, and nothing validated them, so a
document-derived entity type or property name could close the current
Cypher token early and append arbitrary statements (e.g. DETACH DELETE),
running with the application's database credentials.

- New shared semantica/graph_store/query_sanitize.py: sanitize_identifier()
  generalizes age_store.py's existing _sanitize_label/_sanitize_rel_type
  (the only backend that already validated this) into a helper the other
  backends can import without an import cycle with graph_store.py/methods.py.
- Applied at every label/relationship-type/property-key interpolation site
  in amazon_neptune.py, neo4j_store.py, falkordb_store.py, graph_store.py
  (degree_centrality's own query builder), and methods.py
  (update_relationship's own query builder) — create_node, create_nodes,
  create_relationship, get_nodes, get_relationships, get_neighbors,
  shortest_path, update_node, create_index, and all relationship-type
  filters.
- depth/max_depth path-length parameters are also cast to int before
  interpolation as defense-in-depth (they're already typed int, but
  Python doesn't enforce that at runtime).

Added tests/graph_store/test_cypher_injection.py (12 tests covering the
sanitizer directly and reproducing the advisory's injection payload
against Neptune/Neo4j/FalkorDB create_node/create_relationship — asserts
the malicious query is never built or sent), plus regression tests for
graph_store.py's degree_centrality and methods.py's update_relationship.
Full graph_store test suite (224 tests) passes with no regressions.

* fix(graph-store): prevent depth-based Cypher injection

* test(graph-store): tighten injection regression assertions

* docs(changelog): add PR #910 (GHSA-482h Cypher injection) entry

---------

Co-authored-by: Sameer6305 <sskadam6305@gmail.com>
2026-08-11 16:24:28 +05:30

82 lines
3.0 KiB
Python

import unittest
from unittest.mock import MagicMock, patch
from semantica.graph_store import methods
from semantica.graph_store.registry import method_registry
class TestGraphStoreMethods(unittest.TestCase):
def setUp(self):
# Reset global store
methods._reset_store()
# Mock GraphStore
self.mock_store = MagicMock()
self.mock_store_patcher = patch('semantica.graph_store.methods.GraphStore', return_value=self.mock_store)
self.MockGraphStore = self.mock_store_patcher.start()
def tearDown(self):
self.mock_store_patcher.stop()
methods._reset_store()
# Unregister custom methods if any
method_registry.unregister("node", "custom_create")
def test_create_node_default(self):
# Setup
labels = ["Person"]
props = {"name": "Alice"}
self.mock_store.create_node.return_value = {"id": 1, "labels": labels, "properties": props}
# Execute
result = methods.create_node(labels, props)
# Verify
self.mock_store.create_node.assert_called_once_with(labels, props)
self.assertEqual(result["id"], 1)
def test_create_node_custom(self):
# Register custom method
mock_custom = MagicMock(return_value={"id": 99, "custom": True})
method_registry.register("node", "custom_create", mock_custom)
# Execute
result = methods.create_node(["Person"], {"name": "Bob"}, method="custom_create")
# Verify
mock_custom.assert_called_once()
self.mock_store.create_node.assert_not_called()
self.assertEqual(result["id"], 99)
def test_execute_query_default(self):
# Setup
query = "MATCH (n) RETURN n"
self.mock_store.execute_query.return_value = {"records": []}
# Execute
result = methods.execute_query(query)
# Verify
self.mock_store.execute_query.assert_called_once_with(query, None)
def test_update_relationship_rejects_malicious_property_key(self):
"""Regression test for GHSA-482h-hw99-h62p: update_relationship()
interpolates property keys directly into a Cypher SET clause
(methods.py's own query builder, not delegated to the backend
store), so an unvalidated key was a direct injection point."""
from semantica.utils.exceptions import ValidationError
evil_key = "x} MATCH (victim) DETACH DELETE victim //"
with self.assertRaises(ValidationError):
methods.update_relationship(1, {evil_key: "value"})
self.mock_store.execute_query.assert_not_called()
def test_update_relationship_with_legitimate_keys_still_works(self):
self.mock_store.execute_query.return_value = {
"records": [{"id": 1, "type": "KNOWS"}]
}
result = methods.update_relationship(1, {"weight": 0.5})
query = self.mock_store.execute_query.call_args[0][0]
self.assertIn("r.weight = $weight", query)
self.assertNotIn("DETACH DELETE", query)
if __name__ == '__main__':
unittest.main()