title: Write or Fetch Procedure Called Through a Read-Only Graph Query Tool
id: ATR-2026-02648
rule_version: 2
status: experimental
description: |
  Detects a query, carried in the argument of a tool that declares itself READ-ONLY, that calls a procedure the read-only mode was supposed to exclude: `CALL apoc.cypher.doIt`, `apoc.cypher.runFile`, `apoc.periodic.submit`, `apoc.trigger.add`, `apoc.load.json`, `apoc.export.*`, `dbms.security.*`, or a Cypher write clause such as `DETACH DELETE` / `CREATE USER` / `GRANT ROLE`. CVE-2026-35402 (mcp-neo4j-cypher) is the reference case: read-only was enforced by inspecting the query's top-level clause, and `CALL` is not a write clause, so every APOC procedure behind it ran with full authority. The KQL sibling `| externaldata(...)` is included because it is the same move in a different query language -- a read verb that reaches outside the database.
  WHY ATR IS BLIND HERE TODAY. Measured on this repo's engine, the SQL equivalents of these payloads are caught (ATR-2026-01601, ATR-2026-00066) and the Cypher/KQL forms produce nothing but the tool-name noise rule ATR-2026-00099. ATR knows SQL and does not know the other query languages.
  WHY VERSION 1 WAS WITHDRAWN. It required only that the TOKEN `read-only`, `readonly`, `read_query` or `neo4j_read` appear within 240 characters of a procedure call. Adversarial review fired it on 13 of 16 benign probes, none of which involved an agent at all: the APOC manual sentence "the first statement must be read-only" beside `CALL apoc.periodic.iterate(...)`; a TypeScript `readonly database: string` beside a purge query (the `readonly` modifier is in every typed codebase); the Cypher manual page for DETACH DELETE; the Azure KQL page for `externaldata`; an onboarding runbook granting a read-only reporting role; a CI teardown log; a docker-compose file; a CHANGELOG; a Traditional-Chinese operations note; the CVE-2026-35402 advisory itself; and the hardening checklist that enumerates the very procedures the tool must reject -- which the author believed had been excluded by requiring a following `(`, but a checklist writes the parentheses too.
  WHAT VERSION 2 REQUIRES INSTEAD. The event must BE a call to a read-scoped tool: a tool-invocation object whose tool name is read-scoped, or one carrying `read_only: true`. Prose that discusses such a tool no longer qualifies. A second round of probes -- benign traffic through a genuinely read-scoped tool -- forced three more constraints: the procedure must sit inside an ARGUMENT (a result set carrying stored migration scripts is not a query); it must sit at a statement boundary or at the head of a quoted argument value (`CONTAINS 'DETACH DELETE n'` is a search FOR the clause); and `apoc.create.*` / `apoc.export.*` are narrowed so that virtual-node helpers and the streaming schema dump `apoc.export.cypher.schema(null, {stream:true})`, which writes nothing, do not count.
  ATR-2026-02100 already covers `apoc.load.json('file:///...')` -- the local file-scheme case. This rule does not restate it; what it adds is the read-only-mode bypass, whatever the procedure's argument happens to be.
author: ATR Community
date: 2026/08/23
schema_version: '0.1'
detection_tier: pattern
maturity: test
severity: high
references:
  cve:
    - CVE-2026-35402
  cwe:
    - CWE-863
    - CWE-89
    - CWE-693
  owasp_llm:
    - LLM06:2025
  owasp_agentic:
    - ASI03:2026
  mitre_atlas:
    - AML.T0053 - AI Agent Tool Invocation
  external:
    - https://nvd.nist.gov/vuln/detail/CVE-2026-35402
compliance:
  owasp_agentic:
    - id: ASI03:2026
      context: The agent obtains write and outbound-fetch authority through a tool that was granted read authority only; the boundary is crossed inside the query language.
      strength: primary
  owasp_llm:
    - id: LLM06:2025
      context: 'Excessive agency: the effective permission of a tool is wider than the permission its declaration and its name assert.'
      strength: primary
  eu_ai_act:
    - article: '15'
      context: Article 15 requires high-risk systems to be resilient against attempts to exceed their intended operation; a read-only gate defeated inside the query text is exactly such an attempt.
      strength: primary
    - article: '9'
      context: Tool-permission drift is a documented risk for agent deployments; these detections are the Article 9 monitoring control for it.
      strength: secondary
    - article: '12'
      context: Article 12 traceability -- a write that arrived through a read tool leaves no other record that the mode was bypassed.
      strength: secondary
  nist_ai_rmf:
    - function: Govern
      subcategory: GV.4.1
      context: Detects circumvention of the least-privilege control that scopes a database tool to reads.
      strength: primary
    - function: Manage
      subcategory: MG.2.3
      context: Runtime treatment for the risk that query-language features exceed the declared tool permission.
      strength: primary
    - function: Measure
      subcategory: MS.2.7
      context: Detection events evidence whether read-only enforcement actually held, rather than being assumed from configuration.
      strength: secondary
  iso_42001:
    - clause: '8.1'
      context: 'Clause 8.1 operational control: a read-scoped integration that can write or fetch is outside the operating envelope that was approved.'
      strength: primary
    - clause: '9.1'
      context: Monitoring under clause 9.1 must cover whether declared tool permissions are the effective ones.
      strength: secondary
    - clause: '6.2'
      context: Keeping data-store access least-privileged is an AIMS objective under clause 6.2; this rule is its runtime control for graph and log query languages.
      strength: secondary
tags:
  category: privilege-escalation
  subcategory: query-language-readonly-bypass
  scan_target: both
  confidence: medium
  suppress_in_code_blocks: true
agent_source:
  type: mcp_exchange
  framework:
    - any
  provider:
    - any
detection:
  conditions:
    - field: content
      operator: regex
      value: ^\s{0,8}\{[^{}]{0,160}(?:["'](?:tool|tool_name|toolName|name|function|mcp_tool)["']\s*:\s*["'][\w.\-]{0,30}(?:read[_.\-]?(?:only|neo4j|cypher|graph|query|kql|kusto)|(?:neo4j|cypher|graph|kusto|kql)[\w.\-]{0,8}read)[\w.\-]{0,20}["']|["']?read_?only["']?\s*:\s*(?:true|["'](?:on|yes|1)["']))
      description: The event is an invocation of a read-scoped query tool, or carries an explicit read_only flag
    - field: content
      operator: regex
      value: '["''](?:arguments|args|input|params|query|cypher|statement|stmt|kql|sql|q|command|script|text)["''][\s\S]{0,240}(?:^|[\s;,{(]|:\s{0,4}[\\]{0,2}["''\x60]\s{0,4})(?:CALL\s+apoc\.(?:cypher\.(?:doIt|runFile|runSchemaFile|runMany|runWrite)|periodic\.(?:submit|repeat|iterate|commit)|trigger\.(?:add|install)|refactor\.[\w]{1,24}|create\.(?:node|nodes|relationship|relationships|addLabels|removeLabels|setProperty|setProperties|setLabels|setRelProperty|uuids)|merge\.[\w]{1,24}|load\.(?:json|csv|xml|jdbc)(?:Params)?|systemdb\.[\w.]{1,24})\s*\(|CALL\s+apoc\.export\.[\w.]{1,24}\s*\(\s*[\\]{0,2}["''\x60]|DETACH\s+DELETE\s+[\w$(]|CREATE\s+(?:USER|ROLE)\s+[\w$]|DROP\s+(?:USER|ROLE)\s+[\w$]|dbms\.security\.[\w]{1,32}\s*\(|GRANT\s+(?:ROLE|ALL|WRITE|ADMIN)\s+[\w$]|\|\s*externaldata\s*\()'
      description: A write, administrative or outbound-fetch procedure is actually invoked inside a call argument
  condition: all
  false_positives:
    - 'A read-scoped tool asked to EXPLAIN or PROFILE a write statement. `{"tool":"read_neo4j_cypher","arguments":{"query":"EXPLAIN MATCH (n:Stale) DETACH DELETE n"}}` plans the write without running it. Measured residual: 1 of 28 benign probes. Excluding it needs a negative lookbehind, which RE2 does not have and the array condition format cannot express; a read-only tool being asked to plan a destructive statement is worth the alert'
    - A migration or admin job whose tool is genuinely named read_* and legitimately writes
    - Advisory text for CVE-2026-35402 quoting the payload, if it is written as a tool-invocation object rather than as prose
response:
  actions:
    - alert
  auto_response_threshold: high
  message_template: |
    [ATR-2026-02648] A query issued through a read-only graph/log tool calls a write, administrative or outbound-fetch procedure (apoc.cypher.doIt, apoc.periodic.*, apoc.trigger.*, apoc.load.*, dbms.security.*, DETACH DELETE, CREATE USER, KQL externaldata). Reference: CVE-2026-35402. The tool''s read-only promise did not hold; verify what the query changed.
confidence: 70
