GitHub Advisoryadvisory · PyPI

GHSA-pmch-g965-grmr Langroid: SQLChatAgent _validate_query blocklist misses pg_read_file family enabling arbitrary file read

HIGH Published 2026-07-02 Modified 2026-07-02 Official page ↗
Aliases
CVE-2026-50180
Ecosystem
PyPI
Packages
langroid
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-22, CWE-89
Reviewed
2026-07-02

Affected packages

• PyPI langroid: < 0.64.0 → fixed in 0.64.0

Description

Summary `SQLChatAgent` in `langroid` ships a `_validate_query` defense-in-depth layer whose `_DANGEROUS_SQL_PATTERNS` regex blocklist enumerates dangerous SQL primitives by specific function name. The list misses the canonical PostgreSQL filesystem-disclosure family `pg_read_file()`, `pg_stat_file()`, `pg_ls_logdir()`, `pg_ls_waldir()`, `pg_current_logfile()` (and similar `SELECT`-shaped functions in the same family). It also leaves SQL Server `OPENDATASOURCE` and SQLite `ATTACH '' AS x` (DATABASE keyword omitted) unblocked. An attacker able to shape the LLM's generated SQL (directly via prompt input or transitively via prompt-injection in data the LLM ingests) can read arbitrary files from the PostgreSQL host through ordinary `SELECT` queries, even with the agent's strict default configuration (`allow_dangerous_operations=False`, `allowed_statement_types=['SELECT']`). The payloads survive the statement-type allowlist (each is a `SELECT`) and pass through the regex blocklist (none of the function names match), then reach the live SQLAlchemy engine via `SQLChatAgent.run_query`. Affected versions `langroid` `\b` literals. PostgreSQL ships several near-name functions on the same primitive that none of these match: | Function | What it returns | Matched by blocklist? | |---|---|---| | `pg_read_server_file('/path')` | file contents | yes (`pg_read_server_files?`) | | `pg_read_binary_file('/path')` | binary contents | yes | | `pg_ls_dir('/path')` | directory listing | yes | | `pg_read_file('/path')` | file contents | no (no `_server_` infix) | | `pg_stat_file('/path')` | size, mtime, ctime, atime, isdir | no | | `pg_ls_logdir()` | filenames in PostgreSQL log dir | no | | `pg_ls_waldir()` | WAL filenames and sizes | no | | `pg_ls_tmpdir()` | temp-dir listing | no | | `pg_ls_archive_statusdir()` | archive-status directory listing | no | | `pg_current_logfile()` | active server log path | no | Each of these is a `SELECT`-shaped function call. They pass the `sqlglot_exp.Select`-only statement-type allowlist applied at `sql_chat_agent.py:583-614`, then evade the regex blocklist (their names contain no token the blocklist enumerates), then reach the SQLAlchemy `session.execute(text(query))` sink inside `SQLChatAgent.run_query` (line 631 onwards). Two non-PostgreSQL secondary gaps with the same regex-enumeration shape: • The SQLite pattern `\battach\s+database\b` requires the literal `DATABASE` keyword. Per the SQLite grammar (https://www.sqlite.org/lang_attach.html) the keyword is optional: `ATTACH '/path/to/db' AS x` is valid syntax and matches no entry in the blocklist. Whether the agent rejects this via the statement-type allowlist depends on how the configured `sqlglot` dialect parses it; on PostgreSQL dialect parsing fails (sqlglot returns no `Select`) and the statement-type check rejects, but a SQLite-dialect SQLChatAgent (`database_uri="sqlite:///..."`) returns the statement as `sqlglot_exp.Attach`, which is not in the agent's `kind_map`, so the generic `type(stmt).__name__.upper()` branch produces `"ATTACH"`. That string is not in `_DEFAULT_ALLOWED_STATEMENTS` so the allowlist saves it here; however any deployment that extends `allowed_statement_types` to include `"ATTACH"` (e.g. to permit cross-schema connectivity) loses this fallback and the regex misses. • The MSSQL pattern `\bopenrowset\b` blocks `OPENROWSET` but not the closely-related `OPENDATASOURCE` function. Both can read remote/UNC files and execute remote queries via an ad-hoc connection string, e.g. a `SELECT` against `OPENDATASOURCE('SQLNCLI11','Server=remote;Trusted_Connection=yes')` qualified down to `master.sys.tables`. Attack scenario `SQLChatAgent.run_query` (line 617 of `sql_chat_agent.py`) calls `self._validate_query(query)` (line 631) on the LLM-generated SQL. The LLM-generated SQL is shaped by upstream prompt content that crosses the trust boundary: the user message, any tool result the LLM is asked to summarise, any document the agent retrieves, and any row the agent reads back from its own database (the `RunQueryTool` result is fed back into the LLM history at `sql_chat_agent.py:712-720` of the same release). The default config in `SQLChatAgentConfig` (lines 183-184) sets `allow_dangerous_operations=False` and `allowed_statement_types=["SELECT"]`, which is the configuration `_validate_query` was added to support. The bypass primitives below are reachable under this default config because each is a syntactic `SELECT` whose function-call argument is the disclosure vector. Proof of concept `poc.py` (single-file, no external services beyond a transient PostgreSQL spawned via `testing.postgresql`): ```python """ PoC: SQLChatAgent _validate_query bypass via PostgreSQL file-disclosure family pg_read_file / pg_stat_file / pg_ls_logdir / pg_ls_waldir / pg_current_logfile. """ import os import re import sys from typing import List, Optional PKG = "/tmp/poc-langroid-bypass/venv/lib/python3.12/site-packages/langroid" SRC = f"{PKG}/agent/special/sql/sql_chat_agent.py" assert os.path.exists(SRC), f"Missing pinned langroid source: {SRC}" import sqlglot from sqlglot import expressions as sqlglot_exp def load_patterns_from_pinned_source(): """Extract _DANGEROUS_SQL_PATTERNS + _DEFAULT_ALLOWED_STATEMENTS from the pinned langroid 0.63.0 sql_chat_agent.py without instantiating the full agent stack (which needs an LLM config).""" with open(SRC) as f: source = f.read() block = re.search( r"_DANGEROUS_SQL_PATTERNS:[^=]*=\s*\[(.*?)\]\s*\n", source, re.DOTALL, ) ns = {"re": re, "List": list} patterns = eval("[" + block.group(1) + "]", ns) allowed = eval( re.search( r"_DEFAULT_ALLOWED_STATEMENTS:\s*List\[str\]\s*=\s*(\[.*?\])", source, re.DOTALL, ).group(1) ) return patterns, allowed def validate_query(query, patterns, allowed_statements, dialect="postgres"): """Faithful reimple…

Related

Alias · CVE (1)

  • CVE-2026-50180 Langroid is a framework for building large-language-model-powered applications. vulnerability

Weakness · CWE (2)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
  • CWE-89 Improper Neutralization of Special Elements used in an SQL Command weakness

References