Query validation decisions (QV)#

Decisions behind the Cypher validator. For the flow itself, the allowlist, and the test workflow, see query validation.

Primary code: reporting/services/query_validator.py, tests/unit/reporting/services/query_validator_test.py, tests/data/query-fuzzing.csv.

QV-001 — Errors block, warnings never do#

Applies to: validate_queryValidationResult(errors, warnings)

Layers 1 and 2 (the EXPLAIN read-only check and the SSRF/admin/procedure guard) produce errors. Layers 3 and 4 (CyVer SchemaValidator and PropertiesValidator, both run via asyncio.to_thread) produce warnings only.

Why: CyVer reasons about a schema snapshot. Blocking on it would reject valid queries whenever the graph is mid-sync, and the failure would look like a validator bug rather than a stale schema.

QV-002 — Procedures are allowlisted, not denylisted#

Applies to: _DEFAULT_ALLOWED_PROCEDURES, QUERY_VALIDATOR_ALLOWED_PROCEDURES

Every CALL <procedure> is matched against an allowlist of side-effect-free schema procedures. Extend it with exact names or namespace prefixes (apoc.).

Why: the set of dangerous procedures is open-ended and grows with every plugin. The set of procedures a reporting tool needs is small and known.

Note: a namespace prefix in QUERY_VALIDATOR_ALLOWED_PROCEDURES also drops that namespace’s dangerous-function guard. Granting apoc. is therefore a larger decision than it looks.

QV-003 — The guard scans three forms of the query, and comments are checked two-sided#

Applies to: _scan_for_dangerous_constructs

The original text, the comment-stripped form, and the unicode-decoded form are all scanned for LOAD CSV, the USE clause, admin/catalog commands, and the apoc.cypher.*/gds.*/ai.*/genai.* function namespaces.

Why two-sided on comments: scanning the comment-stripped form catches CALL /* x */ apoc.; scanning the original catches // inside a string literal (a URL, say) where the stripper would hide a following CALL apoc.. Either scan alone has a bypass.

QV-004 — Every attack vector tried is recorded, including the ones that were already blocked#

Applies to: tests/data/query-fuzzing.csv

One row per technique (Technique, Cypher, Result, Blocked_By, Notes), with matching assertions in query_validator_test.py.

Why: the corpus is the record of what has been tried, not just what failed. A vector that is already blocked still belongs in it, because the next person to consider that technique should find it answered rather than re-derive it.

Do, when adding a case: confirm whether Neo4j actually executes the vector (logs: ExternalResourceFailed, or write side effects) → add the CSV row → add the unit test → if it is not blocked, fix query_validator.py first and then prove it with the test.