GHSA-2pq5-3q89-j7cc
GHSA-2pq5-3q89-j7cc is a CWE-74 vulnerability in langroid. O3 Security confirms whether GHSA-2pq5-3q89-j7cc is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Langroid: Neo4jChatAgent executes LLM-generated Cypher without validation (prompt-to-Cypher injection; config-conditional RCE), mirroring the SQLChatAgent bug fixed in CVE-2026-25879
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-2pq5-3q89-j7cc.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
langroidReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Neo4jChatAgent passes LLM-generated Cypher queries straight to the Neo4j driver with no validation, no statement-type allowlist, and no opt-out gate. The query text is influenceable by prompt injection (direct user input or indirect content the agent reads back via RAG), so an attacker who can influence the prompt can read or destroy all graph data and, when APOC or dbms.security procedures are enabled on the server, achieve OS-command and filesystem access. This is the same defect class and threat model as the SQLChatAgent prompt-to-SQL-to-RCE issue fixed in version 0.63.0 (CVE-2026-25879); that fix did not extend to the neo4j module.
Technical detail
Untrusted-input to sink trace (reviewed on langroid HEAD b9df06f, v0.65.3):
-
Tool schemas accept raw query text from the LLM.
langroid/agent/special/neo4j/tools.py:4-9(CypherRetrievalTool.cypher_query: str) and:15-21(CypherCreationTool.cypher_query: str). These tools are enabled unconditionally inneo4j_chat_agent.py:412-419(enable_message([GraphSchemaTool, CypherRetrievalTool, CypherCreationTool, DoneTool])). -
Read path.
neo4j_chat_agent.py:300cypher_retrieval_tool(msg) ->:325query = msg.cypher_query ->:328self.read_query(query) ->:223session.run(query, parameters). The LLM-controlled string is the first positional argument to session.run; parameters is None. No validation occurs between :325 and :223. -
Write path.
neo4j_chat_agent.py:338cypher_creation_tool(msg) ->:348query = msg.cypher_query ->:351self.write_query(query) ->:276session.write_transaction(lambda tx: tx.run(query, parameters)). The only inspection of the string (write_query :251-264) is a query.upper() substring test for CREATE/MERGE/CONSTRAINT/INDEX whose sole effect is setting self.config.database_created = True; it blocks nothing. A query such asMATCH (n) DETACH DELETE n(the same statement the built-in remove_database helper runs at :287-293) passes unrestricted. -
Guarded-sibling contrast proving the incomplete fix. The SQLChatAgent, patched for the parent CVE, validates every query before execution.
langroid/agent/special/sql/sql_chat_agent.py:256defines allow_dangerous_operations: bool = False (opt-in gate);:618_validate_query runs (a) a dangerous-pattern regex blocklist (_DANGEROUS_SQL_PATTERNS, :628-641), (b) a sqlglot statement-type allowlist defaulting to SELECT-only (:643-686), and (c) an AST-level dangerous-function blocklist (:687-704). run_query calls rejection = self._validate_query(query) at:721before executing. The neo4j read_query/write_query paths have no equivalent: a grep for _validate_query/allow_dangerous in neo4j_chat_agent.py returns nothing. The defense exists for SQL and is simply absent for Cypher, which is the definition of an incomplete fix for the same prompt-to-query-language injection class.
Proof of concept (static, no third-party systems, no live Neo4j required)
Step 1 (presence vs absence of the guard, one command):
grep -n "_validate_query\|allow_dangerous" langroid/agent/special/sql/sql_chat_agent.py # SQL: many hits (gate + validator + call site :721)
grep -n "_validate_query\|allow_dangerous" langroid/agent/special/neo4j/neo4j_chat_agent.py # neo4j: ZERO hits
Step 2 (sink trace is direct, no interposed check):
read_query: msg.cypher_query (line 325) -> read_query(query) (328) -> session.run(query, parameters) (223)
write_query: msg.cypher_query (348) -> write_query(query) (351) -> tx.run(query, parameters) (276)
The only string inspection (write_query 251-264) is a .upper() substring test that only sets database_created=True and rejects nothing. The asymmetry (validator + opt-out gate enforced on every SQL query at sql_chat_agent.py:721; nothing on either Cypher path) is the deterministic artifact: the same project, for the same injection class, guards one query language and not the other. A dynamic confirmation against an operator-owned disposable Neo4j (create a CSPRNG-marked node via cypher_creation_tool, read it back via cypher_retrieval_tool, then DETACH DELETE it) reproduces the read/write/destroy primitive without any third-party system.
Impact
An attacker who can influence the agent prompt (directly, or indirectly via content the agent reads back through RAG) controls the executed Cypher. Floor (no extra config, not contingent): unauthorized read of all graph data via cypher_retrieval_tool and full write/destroy (including MATCH (n) DETACH DELETE n) via cypher_creation_tool, plus the built-in LOAD CSV remote-fetch (SSRF) primitive. Ceiling (config-conditional, when APOC / dbms.security procedures are granted to the DB role, a common deployment): apoc.load.* (SSRF + remote/local file read), apoc.cypher.runFile / apoc.import.* / apoc.export.* (filesystem), and CALL dbms.* admin procedures, i.e. the Cypher analogue of the parent's COPY ... FROM PROGRAM RCE primitive. This mirrors the privileged-role contingency the parent advisory (CVE-2026-25879) accepted.
Suggested fix
Mirror the SQLChatAgent fix in the neo4j module: (1) add allow_dangerous_operations: bool = False to Neo4jChatAgentConfig with a read-only default for cypher_retrieval_tool; (2) add a _validate_cypher(query, write) method that, unless allow_dangerous_operations is True, blocks CALL apoc., CALL dbms., CALL db.* admin procedures, LOAD CSV, and any procedure/function touching filesystem/network/OS, and (for the read path) rejects write clauses (CREATE/MERGE/SET/DELETE/DETACH DELETE/REMOVE/DROP); (3) enforce it before session.run (read_query :223) and tx.run (write_query :276), returning the rejection to the LLM the way run_query does at sql_chat_agent.py:721; (4) document, as the SQL config does, that LLM-generated Cypher is prompt-injectable and that allow_dangerous_operations should only be set with a least-privilege Neo4j role. I am happy to send this as a PR if useful.
Relationship to the parent advisory
GHSA-mxfr-6hcw-j9rq / CVE-2026-25879 (Langroid SQLChatAgent prompt-to-SQL injection leading to RCE; Critical; fixed 0.63.0, SQLChatAgent only). This report is the same injection class in the still-unpatched sibling Neo4jChatAgent.
Severity note (honest, both ways)
Filed as High to reflect the non-contingent floor (unrestricted attacker-steered graph read/write/destroy + LOAD CSV SSRF, present regardless of APOC). The ceiling is Critical and RCE-equivalent when APOC / admin procedures are enabled on the DB role, at parity with the parent CVE-2026-25879 which was rated Critical under an equivalent privileged-role contingency. Please rate per your deployment assumptions.
Resolution (maintainer)
Fixed in 0.65.5. Both Neo4jChatAgent (Cypher) and the sibling ArangoChatAgent (AQL, raised in the follow-up) now mirror the SQLChatAgent controls: a new allow_dangerous_operations config gate (default False), with the retrieval tool restricted to read-only queries and both tools rejecting code-execution / file / network primitives (LOAD CSV, apoc.*, dbms.*, CALL db.* for Cypher; user-defined namespace::func calls for AQL) unless the operator opts in. Validation is enforced at the tool handlers, so internal schema/maintenance calls are unaffected. Upgrade to 0.65.5 and run the agents against a least-privilege database role.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | langroid | all versions | 0.65.5 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for langroid. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update langroid to 0.65.5 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-2pq5-3q89-j7cc is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-2pq5-3q89-j7cc is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-2pq5-3q89-j7cc. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-2pq5-3q89-j7cc in your dependencies?
O3 detects GHSA-2pq5-3q89-j7cc across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.