Skip to content

fix(db_neo4j): don't reject read-only Cypher that names properties like write keywords#1592

Open
Ansh-Karnwal wants to merge 2 commits into
rocketride-org:developfrom
Ansh-Karnwal:fix/RR-1375-neo4j-keyword-scan-false-positives
Open

fix(db_neo4j): don't reject read-only Cypher that names properties like write keywords#1592
Ansh-Karnwal wants to merge 2 commits into
rocketride-org:developfrom
Ansh-Karnwal:fix/RR-1375-neo4j-keyword-scan-false-positives

Conversation

@Ansh-Karnwal

@Ansh-Karnwal Ansh-Karnwal commented Jul 15, 2026

Copy link
Copy Markdown

Fixes #1375

Problem

db_neo4j's read-only gate _is_cypher_safe (in nodes/src/nodes/db_neo4j/utils.py) matches write keywords with \bKEYWORD\b. That word-boundary matches a keyword used as an attribute name, so a perfectly read-only query like:

MATCH (n) RETURN n.delete

is wrongly rejected as a write. This is the same class of bug that was fixed for db_arango in #1294.

Why neo4j needs its own fix: db_arango can lean on its EXPLAIN-plan gate as the authoritative read-only check, so there the keyword scan is only a soft backstop. db_neo4j has no such gate — _validate_query runs EXPLAIN for syntax only — so the keyword scan is the primary read-only gate here.

Change

  • _UNSAFE_CYPHER: add a leading negative look-behind (?<![\w.]) so a write keyword used as attribute access or a compound identifier (n.delete, last_set) is not matched.
  • _is_cypher_safe: strip string literals and comments in a single alternation pass, so a keyword inside a string (WHERE n.op = "CREATE") doesn't cause a false rejection, and a stray quote inside a comment can't make string-stripping span newlines and swallow a real trailing write keyword. This mirrors the arango implementation.

A bare label or identifier spelled exactly like a keyword (e.g. a node label literally named Delete) can still match — a text scan can't tell a keyword from an identifier. Closing that gap needs a real plan-based read-only gate, which is tracked separately and left out of scope here to keep this change focused.

Tests

Adds nodes/test/test_db_neo4j.py covering the safety gate (attribute access, string literals, comments, case-insensitivity), _parse_is_valid, and _strip_ns. utils.py imports only the stdlib, so the gate is testable without a live Neo4j.

builder nodes:test        # or: pytest nodes/test/test_db_neo4j.py

Read-only queries such as MATCH (n) RETURN n.delete now pass the gate; writes such as MATCH (n) DETACH DELETE n are still rejected.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Neo4j read-only query validation to more accurately detect unsafe write/admin constructs.
    • Reduced false positives when keyword-like text appears in identifiers, comments, or string literals.
    • Kept case-insensitive handling for unsafe keyword detection.
  • Tests

    • Added standalone pytest coverage for Cypher safety checks, input parsing behavior, and namespace prefix stripping.

…ke write keywords

_is_cypher_safe matched write keywords with \bKEYWORD\b, so a read-only query
that references a property or identifier spelled like a write keyword (e.g.
MATCH (n) RETURN n.delete) was wrongly refused as a write.

Add a leading negative look-behind (?<![\w.]) so a keyword used as attribute
access or a compound identifier is not matched, and strip string literals before
comments so a keyword inside a string -- or a // sequence inside a string --
neither triggers a false rejection nor hides a real trailing write keyword.

This mirrors the accepted db_arango fix (rocketride-org#1294). Unlike db_arango, db_neo4j has
no EXPLAIN-plan gate (_validate_query runs EXPLAIN for syntax only), so this
keyword scan is the primary read-only gate; a bare label or identifier spelled
exactly like a keyword can still match, which a text scan cannot disambiguate.
That deeper plan-based gate is tracked separately and is out of scope here.

Adds nodes/test/test_db_neo4j.py covering the safety gate (attribute access,
string literals, comments), isValid parsing, and namespace stripping.

Test plan:
  # utils imports only the stdlib, so the gate is verifiable without infra:
  python3 - <<'PY'
  import importlib.util; from pathlib import Path
  s = importlib.util.spec_from_file_location('u', 'nodes/src/nodes/db_neo4j/utils.py')
  u = importlib.util.module_from_spec(s); s.loader.exec_module(u)
  assert u._is_cypher_safe('MATCH (n) RETURN n.delete') is True
  assert u._is_cypher_safe('MATCH (n) DETACH DELETE n') is False
  PY
  # full suite (in the repo env):
  builder nodes:test  # or: pytest nodes/test/test_db_neo4j.py
@github-actions github-actions Bot added the module:nodes Python pipeline nodes label Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Neo4j Cypher safety gating

Layer / File(s) Summary
Refine Cypher safety detection
nodes/src/nodes/db_neo4j/utils.py
Unsafe keyword matching now avoids attribute and compound identifiers, while strings and comments are removed before scanning.
Add direct utility coverage
nodes/test/test_db_neo4j.py
Direct-loading tests cover Cypher safety, string and comment handling, validity parsing, and namespace stripping.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: jmaionchi, rod-christensen, stepmikhaylov

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #1375 by preventing keyword false-positives in read-only Cypher while still rejecting actual writes.
Out of Scope Changes check ✅ Passed The string/comment stripping and new tests are directly related to the Cypher safety bug and add no unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: avoiding false rejections of read-only Cypher when property names resemble write keywords.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/db_neo4j/utils.py`:
- Around line 79-85: The query sanitization logic in the visible stripping
sequence must use one combined regex pass for quoted strings, line comments, and
block comments, with alternatives ordered so comments are consumed before quote
matching can cross them. Update the `stripped` processing in the surrounding
utility to remove all three token types left-to-right, preserving escaped-quote
handling and preventing matches from spanning comment boundaries.

In `@nodes/test/test_db_neo4j.py`:
- Around line 85-87: Add a test alongside
test_slashslash_in_string_does_not_hide_write_keyword that passes
_is_cypher_safe a query containing a line comment with an unbalanced quote
followed across a newline by a write keyword, and assert the result is False.
Ensure the case specifically verifies comment quotes cannot hide valid Cypher
write operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76792d02-02c5-4be0-9193-18192e8f9912

📥 Commits

Reviewing files that changed from the base of the PR and between f41afb5 and 03fe6a0.

📒 Files selected for processing (2)
  • nodes/src/nodes/db_neo4j/utils.py
  • nodes/test/test_db_neo4j.py

Comment thread nodes/src/nodes/db_neo4j/utils.py Outdated
Comment thread nodes/test/test_db_neo4j.py
… safety-gate bypass

Stripping string literals and comments in separate passes let an unbalanced
quote inside a comment swallow a trailing write keyword across newlines: the
string pattern's character class matches newlines, so a lone `"` in a comment
consumed everything up to the next quote. A query like

    MATCH (n)
    // start "
    DELETE n
    // end "

got reduced to `MATCH (n)` and slipped past _is_cypher_safe as read-only.

Fold the four patterns into a single alternation so tokens are matched
left-to-right and a comment consumes its own contents before a string
pattern can be tricked by a quote inside it. Add a regression test.
@Ansh-Karnwal

Copy link
Copy Markdown
Author

Addressed the CodeRabbit review in 4c1945e: folded the string/comment stripping into a single alternation pass to close the safety-gate bypass, and added a regression test for the unbalanced-quote-in-comment case. Confirmed the gate now rejects the exploit query while still allowing keyword-named attributes/strings.

@dsapandora

Copy link
Copy Markdown
Collaborator

The fix itself is right and still needed, but develop moved under you: #1611 (merged after you opened this) migrated Neo4j onto the shared graph base, so nodes/src/nodes/db_neo4j/utils.py no longer exists — that's why the PR now shows CONFLICTING.

The exact same read-only gate now lives in packages/ai/src/ai/common/graph/cypher_safety.py (is_cypher_safe / _UNSAFE_CYPHER), and it still has both bugs you caught: the \b(?:CREATE|DELETE|…) word-boundary (matches n.delete), and comment-only stripping that doesn't strip string literals (n.op = "CREATE"). Could you re-point this PR there? Applying your (?<![\w.]) look-behind + single-pass string/comment strip in that one file fixes it for every graph node at once — the gate is also called from graph_instance_base.py (lines 225, 341, 519), not just Neo4j.

For the test: utils.py was stdlib-only so you loaded it via importlib; the shared module is a normal import now, so from ai.common.graph.cypher_safety import is_cypher_safe works directly. There's already sibling coverage to match in packages/ai/tests/ai/common/database/test_sql_safety.py and nodes/test/test_graph_falkordb.py. Note your _parse_is_valid / _strip_ns cases don't port — those helpers moved onto the base during the migration.

Thanks for chasing this down — the false-positive analysis is spot on, it just needs to land one layer up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module:nodes Python pipeline nodes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

db_neo4j: read-only keyword scan false-positives on keyword-named properties/labels

2 participants