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:
@@ -40,21 +40,24 @@ async def execute_cypher_query(
|
||||
api_key: str = Depends(verify_api_key)
|
||||
):
|
||||
"""
|
||||
Execute a user-scoped Cypher query.
|
||||
Execute a raw Cypher query (ADMIN/DEBUG — read-only, NOT tenant-scoped).
|
||||
|
||||
The query is automatically scoped to the user's data for security.
|
||||
This prevents users from accessing other users' graph data.
|
||||
**Security model:**
|
||||
- Write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP/DETACH/FOREACH/
|
||||
LOAD CSV) and CALL procedures are rejected with 400.
|
||||
- Execution happens in a read-only Neo4j session as a hard backstop.
|
||||
- Results are NOT restricted to the requesting user's tenant labels —
|
||||
scope your own patterns (e.g. match `User_<Tenant>_Document`).
|
||||
For tenant-scoped access use /graph/nodes instead.
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
{
|
||||
"query": "MATCH (d:Document) RETURN d LIMIT 10",
|
||||
"query": "MATCH (d:User_Llm_Tester_Document:Document) RETURN d LIMIT 10",
|
||||
"parameters": {},
|
||||
"user": "<tenant>"
|
||||
}
|
||||
```
|
||||
|
||||
**Security:** Query is automatically scoped with user label.
|
||||
"""
|
||||
try:
|
||||
return await graph_service.execute_query(
|
||||
|
||||
Reference in New Issue
Block a user