fix(security)!: make raw Cypher endpoints read-only with write-clause denylist
/query/graph scoping was a documented no-op (graph_service returned the query unscoped) and neo4j_client permitted writes; a live probe showed a nonexistent user could read the whole graph. - Add Neo4jClient.execute_read() that opens the session with default_access_mode=READ_ACCESS so the database refuses writes even if validation is bypassed. - GraphService.execute_query() now rejects queries containing CREATE/MERGE/DELETE/DETACH/SET/REMOVE/DROP/FOREACH/LOAD or any CALL (conservative word-boundary denylist on the uppercased query) and executes through the read-only session; the no-op _scope_query_to_user is removed. - Remove the false user-scoping claims from /query/graph (main.py) and /graph/query docs and the CypherQueryRequest model: the endpoints are documented as admin/debug, unscoped read-only (per-tenant label injection for arbitrary Cypher would need a real parser; /graph/nodes remains the tenant-scoped path). - Offline unit tests: denylist coverage (incl. lowercase/multiline/CALL), word-boundary false-positive check, and READ_ACCESS session assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+12
-6
@@ -338,17 +338,23 @@ async def graph_query(
|
||||
api_key: str = Depends(verify_api_key)
|
||||
):
|
||||
"""
|
||||
Execute a Cypher query against the Neo4j knowledge graph.
|
||||
Execute a raw Cypher query against the Neo4j knowledge graph
|
||||
(ADMIN/DEBUG — read-only, NOT tenant-scoped).
|
||||
|
||||
Queries are automatically scoped to the user's data for security.
|
||||
Use this for custom graph traversals beyond what /graph/nodes provides.
|
||||
**Security model:**
|
||||
- Queries containing write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP/
|
||||
DETACH/FOREACH/LOAD CSV) or any CALL are rejected with 400.
|
||||
- Execution happens in a read-only Neo4j session, so writes are refused
|
||||
by the database even if validation is bypassed.
|
||||
- Results are NOT automatically restricted to the requesting user's
|
||||
tenant: an arbitrary query can read any tenant's nodes. Scope your
|
||||
own patterns (e.g. `MATCH (d:User_<Tenant>_Document:Document) ...`).
|
||||
For tenant-scoped access use /graph/nodes instead.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
POST /query/graph?query=MATCH%20(d:Document)-[:MENTIONS]->(p:Person)%20RETURN%20d,p&user=jpmschweitzer
|
||||
POST /query/graph?query=MATCH%20(d:Document)-[:MENTIONS]->(p:Person)%20RETURN%20d,p&user=<tenant>
|
||||
```
|
||||
|
||||
**Security:** All queries are user-scoped to prevent cross-user data access.
|
||||
"""
|
||||
from src.services.graph_service import GraphService
|
||||
|
||||
|
||||
Reference in New Issue
Block a user