Show full advisory descriptionHide advisory 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 '<file>' 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 <= 0.63.0 (latest at the time of this report; PyPI release
2026-05-27). The vulnerable code path is
langroid/agent/special/sql/sql_chat_agent.py::_validate_query, which
consults the module-level _DANGEROUS_SQL_PATTERNS literal at
sql_chat_agent.py:113-141.
Privilege required
Any caller able to influence the LLM-generated RunQueryTool.query string
that reaches SQLChatAgent.run_query. In a typical deployment this is any
client of a SQLChatAgent-backed service, or any upstream data source whose
content the LLM is asked to read and summarise. No PostgreSQL credentials
are required from the attacker; the agent holds them.
Vulnerable code
langroid/agent/special/sql/sql_chat_agent.py:113-141 (the
_DANGEROUS_SQL_PATTERNS literal) and sql_chat_agent.py:546-615 (the
_validate_query method that consults it):
The blocklist is a list of \b<exact-token>\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):
End-to-end reproduction
Run against the latest published langroid release from PyPI; no external
LLM provider, no API key, no Docker, just a transient pg_ctl-managed
PostgreSQL spawned in-process by testing.postgresql. Captured transcript
of the run is below.
Observed transcript (abridged to bypass results; the run also verifies
that the four primitives the current blocklist already covers
(COPY ... TO PROGRAM, pg_read_server_file, pg_read_binary_file,
pg_ls_dir) continue to be REJECTED, confirming the proposed fix is
strictly broader, not narrower):
The headline payload SELECT pg_read_file('langroid_bypass_victim.txt')
returns the marker string verbatim from the file on disk. The same SQL,
issued by an LLM under prompt-injection through any data source the agent
reads, would land identically — the validator is purely a function of the
SQL string and is consulted before the SQLAlchemy execute.
_validate_query is invoked directly rather than through a fully
initialised SQLChatAgent because the agent's __init__ builds the LLM
stack and demands a working LLM API key (or a stub). The security
control under test is purely a function of (query, patterns,
allowed_statements, dialect), so the direct call is observationally
equivalent to a call via run_query. Patterns and allowed-statements are
loaded by reading the pinned sql_chat_agent.py source out of the venv,
guaranteeing no drift between PoC and shipped binary.
Impact
· Arbitrary file read from the PostgreSQL host: pg_read_file() reads
files from PGDATA-relative paths by default and can take absolute paths
when the DB role holds pg_read_server_files (or equivalent in
managed-Postgres setups). For self-managed PostgreSQL deployments the
DB role is frequently a superuser, in which case absolute paths are
always accepted and the impact extends to postgresql.conf,
pg_hba.conf, ~/.pgpass, TLS keys, and any other file readable by
the PostgreSQL OS user.
· Filesystem reconnaissance via pg_stat_file() (file existence,
size, mtime, isdir), pg_ls_logdir(), pg_ls_waldir(),
pg_ls_tmpdir(), pg_ls_archive_statusdir(),
pg_current_logfile().
· MSSQL extension: OPENDATASOURCE reaches remote SQL Servers and
UNC paths, providing arbitrary outbound read + intranet pivot on MSSQL
deployments.
· SQLite extension: ATTACH '<path>' AS schemaname (DATABASE keyword
omitted) allows reading/writing arbitrary SQLite files on deployments
whose allowed_statement_types include "ATTACH".
Suggested fix
Patch _DANGEROUS_SQL_PATTERNS to cover the full family rather than
individual function names. Two compatible approaches; either is enough.
Approach 1 — family-prefix regex (minimal change, simplest to review):
Approach 2 — sqlglot AST walk in addition to regex. sqlglot is already
imported by sql_chat_agent.py; iterate every function-call node
(sqlglot_exp.Anonymous / sqlglot_exp.Func) inside the parsed
statements and reject when the lower-cased name starts with pg_read,
pg_stat, pg_ls, pg_current_logfile, lo_, or matches the MSSQL
extended-procedure prefixes (xp_, sp_oa). AST matching is robust to
whitespace, comments, and case games inside identifiers, at the cost of
broader per-dialect maintenance. For closing the immediate gap, Approach
1 is sufficient.
Regression-test the additions in
tests/main/sql_chat/test_sql_chat_security.py alongside the existing
security tests. A natural 7-case extension covers the 5 PostgreSQL
bypass payloads, the SQLite ATTACH ... AS x form, and the MSSQL
OPENDATASOURCE form.
Fix PR
A private temp-fork PR applying the Suggested fix Approach 1 diff,
plus the regression tests described above, accompanies this advisory:
https://github.com/langroid/langroid-ghsa-pmch-g965-grmr/pull/1
Credit
Reported by tonghuaroot.
What actually happenedHide details
Count Claude-marked single-parent 60933b48, whose first-parent introduces _DANGEROUS_SQL_PATTERNS and _validate_query with per-name PostgreSQL filesystem regexes that miss pg_read_file/pg_stat_file/pg_ls_logdir/pg_ls_waldir/pg_current_logfile. Do not count parent 763d5cba or pre-AI 0.62.0 (no validator; that is GHSA-MXFR/CVE-2026-25879 COPY PROGRAM). Do not count 56e2756e file-tool path traversal. Count PyPI langroid 0.63.0 wheel/sdist blob 887a10a4 as the residual and 00b7dd7b / PyPI 0.64.0 blob a55f6d34 as the family-prefix amendment of that same list.
@@ -1,13 +1,14 @@ """-Agent that allows interaction with an SQL database using SQLAlchemy library. -The agent can execute SQL queries in the database and return the result. +Agent that allows interaction with an SQL database using SQLAlchemy library.+The agent can execute SQL queries in the database and return the result.
Show 84 hidden lines
Functionality includes: - adding table and column context - asking a question about a SQL schema """ import logging+import re from typing import Any, Dict, List, Optional, Sequence, Union from rich.console import Console@@ -24,6 +25,15 @@ except ImportError as e: raise LangroidImportError(extra="sql", error=str(e))+try:+ # sqlglot is required for the statement-type allowlist enforced in+ # `_validate_query`. Importing it at module load ensures the security+ # guarantee cannot be silently bypassed by a partial/stale install.+ import sqlglot+ from sqlglot import expressions as sqlglot_exp+except ImportError as e:+ raise LangroidImportError(extra="sql", error=str(e))+ from langroid.agent.chat_agent import ChatAgent, ChatAgentConfig from langroid.agent.chat_document import ChatDocument from langroid.agent.special.sql.utils.description_extractors import (@@ -96,6 +106,46 @@ SQL_ERROR_MSG = "There was an error in your SQL Query"+# Dialect-specific SQL patterns that enable code execution, arbitrary file+# access, or other escapes from the database engine. Matched against the raw+# query text (case-insensitive) as a defense-in-depth layer in addition to the+# sqlglot-based statement-type allowlist.+_DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [+ # PostgreSQL: COPY ... FROM/TO PROGRAM executes shell commands as the DB+ # server OS user. This is the primitive used in CVE-2026-25879.+ re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),+ # PostgreSQL server-side filesystem access+ re.compile(r"\bpg_read_server_files?\b", re.IGNORECASE),+ re.compile(r"\bpg_read_binary_file\b", re.IGNORECASE),+ re.compile(r"\bpg_ls_dir\b", re.IGNORECASE),+ re.compile(r"\blo_(import|export)\b", re.IGNORECASE),+ # MySQL/MariaDB filesystem+ re.compile(r"\binto\s+(outfile|dumpfile)\b", re.IGNORECASE),+ re.compile(r"\bload_file\s*\(", re.IGNORECASE),+ re.compile(r"\bload\s+data\b", re.IGNORECASE),+ # SQLite: load_extension enables loading arbitrary shared objects;+ # ATTACH DATABASE can read/write arbitrary files.+ re.compile(r"\bload_extension\s*\(", re.IGNORECASE),+ re.compile(r"\battach\s+database\b", re.IGNORECASE),+ # SQL Server: command execution and OLE automation+ re.compile(r"\bxp_cmdshell\b", re.IGNORECASE),+ re.compile(r"\bsp_oacreate\b", re.IGNORECASE),+ re.compile(r"\bsp_oamethod\b", re.IGNORECASE),+ re.compile(r"\bopenrowset\b", re.IGNORECASE),+ re.compile(r"\bbulk\s+insert\b", re.IGNORECASE),+ # Generic: stored-program creation and procedural language extensions+ re.compile(+ r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger)\b", re.IGNORECASE+ ),+ re.compile(r"\bcreate\s+extension\b", re.IGNORECASE),+]+++# Default set of SQL statement types the agent is allowed to execute when+# `allow_dangerous_operations` is False. SELECT-only is safe for Q&A workloads.+_DEFAULT_ALLOWED_STATEMENTS: List[str] = ["SELECT"]++ class SQLChatAgentConfig(ChatAgentConfig): system_message: str = DEFAULT_SQL_CHAT_SYSTEM_MESSAGE user_message: None | str = None@@ -117,6 +167,22 @@ class SQLChatAgentConfig(ChatAgentConfig): max_result_rows: int | None = None # limit query results to this max_retained_tokens: int | None = None # limit history of query results to this+ # --- Security controls (see CVE-2026-25879) ---------------------------+ # By default, the agent only executes SELECT statements and rejects any+ # query that matches a known dangerous pattern (e.g. PostgreSQL+ # `COPY ... FROM PROGRAM`, MySQL `INTO OUTFILE`, SQLite `load_extension`,+ # MSSQL `xp_cmdshell`). The LLM-generated SQL is influenceable by prompt+ # injection — including injection via data the LLM reads back from the
Introduces the SELECT-only validator behind a dialect-aware dangerous-pattern list that names individual PostgreSQL file functions, leaving near-name read, stat, and listing procedures outside the block.
@@ -0,0 +1,197 @@+"""+Unit tests for SQLChatAgent's query validator (CVE-2026-25879 mitigation).++These tests exercise `_validate_query` and `run_query` directly without an
Show 85 hidden lines
+LLM, so they don't require API credentials.+"""++import pytest++from langroid.exceptions import LangroidImportError++try:+ from sqlalchemy import Column, Integer, String, create_engine+ from sqlalchemy.ext.declarative import declarative_base+ from sqlalchemy.orm import sessionmaker+except ImportError as e:+ raise LangroidImportError(extra="sql", error=str(e))++from langroid.agent.special.sql.sql_chat_agent import (+ SQLChatAgent,+ SQLChatAgentConfig,+)+from langroid.agent.special.sql.utils.tools import RunQueryTool++Base = declarative_base()+++class Item(Base):+ __tablename__ = "items"+ id = Column(Integer, primary_key=True)+ name = Column(String, nullable=False)++[email protected]+def session():+ engine = create_engine("sqlite:///:memory:")+ Base.metadata.create_all(engine)+ Session = sessionmaker(bind=engine)+ s = Session()+ s.add(Item(id=1, name="a"))+ s.commit()+ yield s+ s.close()+++def _make_agent(session, **cfg_kwargs):+ cfg = SQLChatAgentConfig(+ database_session=session,+ llm=None,+ use_helper=False,+ **cfg_kwargs,+ )+ return SQLChatAgent(cfg)+++# ---------------------------------------------------------------------------+# Validator unit tests+# ---------------------------------------------------------------------------+++def test_select_allowed_by_default(session):+ agent = _make_agent(session)+ assert agent._validate_query("SELECT * FROM items") is None++[email protected](+ "query",+ [+ "DROP TABLE items",+ "CREATE TABLE x (id int)",+ "ALTER TABLE items ADD COLUMN y int",+ "UPDATE items SET name='b' WHERE id=1",+ "INSERT INTO items (id, name) VALUES (2, 'b')",+ "DELETE FROM items WHERE id=1",+ "TRUNCATE TABLE items",+ ],+)+def test_non_select_blocked_by_default(session, query):+ agent = _make_agent(session)+ rejection = agent._validate_query(query)+ assert rejection is not None+ assert "REJECTED" in rejection+++def test_cve_2026_25879_poc_blocked(session):+ """The exact reproducer from the security advisory must be rejected."""+ agent = _make_agent(session)+ poc = (+ "DROP TABLE IF EXISTS log;\n"
Adds tests confirming the validator accepts plain SELECT, rejects write and schema statements, and blocks the original command-execution payload while leaving database read procedures unexercised.
@@ -17,6 +17,29 @@ automatically extracting schemas from the database. ## Before you begin+!!! warning "Security: SQLChatAgent executes LLM-generated SQL"+ `SQLChatAgent` runs SQL produced by a language model against your+ database. Because the LLM is influenceable by prompt injection — both+ through the user's question and through any data the LLM reads back from
Show 71 hidden lines
+ the database — an attacker who can shape either input can attempt to+ coerce the agent into running dangerous SQL.++ The agent defaults to **SELECT-only** execution (CVE-2026-25879) and+ rejects queries that match known dangerous primitives+ (e.g. PostgreSQL `COPY ... FROM PROGRAM`, MySQL `INTO OUTFILE`, SQLite+ `load_extension`, MSSQL `xp_cmdshell`). See+ [Security configuration](#security-configuration) below.++ Independently of the agent's defenses, you should always:++ - Connect using a **least-privilege database role** that does not have+ `pg_execute_server_program`, `FILE`, `xp_cmdshell`, or other+ privileges enabling code execution or filesystem access.+ - Treat the agent's prompt input as untrusted if it can come from end+ users.+ - Avoid pointing the agent at databases that contain other tenants'+ data.+ !!! note "Data Privacy Considerations" Since the SQLChatAgent uses the OpenAI GPT-4 as the underlying language model, users should be aware that database information processed by the agent may be@@ -100,6 +123,48 @@ agent will automatically generate the file using the built-in Postgres table/col reduces token usage, though it may result in 1-3 additional OpenAI API calls before the final SQL query is generated.+## Security configuration++`SQLChatAgentConfig` exposes two flags that govern which SQL the agent will+execute:++* `allowed_statement_types: List[str] = ["SELECT"]` — the SQL statement+ types the agent is permitted to run. Each query is parsed with+ [`sqlglot`](https://github.com/tobymao/sqlglot); any statement whose top+ level falls outside this list is rejected before it reaches the database.+ Multi-statement queries are validated statement-by-statement, so a+ payload like `SELECT 1; DROP TABLE t` is also rejected.+* `allow_dangerous_operations: bool = False` — when `True`, **disables**+ the statement-type allowlist and the dangerous-pattern blocklist. Only+ set this if (a) the agent's prompt input is fully trusted, and (b) the+ database connection uses a least-privilege role.++In addition to the statement-type check, the agent maintains a dialect-aware+blocklist of patterns that enable code execution, arbitrary file access, or+other escape primitives — for example:++| Dialect | Blocked construct (examples) |+|------------|-------------------------------------------------------------------|+| PostgreSQL | `COPY … FROM/TO PROGRAM`, `pg_read_server_files`, `lo_import/export`, `CREATE EXTENSION`, `CREATE FUNCTION` |+| MySQL | `INTO OUTFILE`, `INTO DUMPFILE`, `LOAD_FILE(…)`, `LOAD DATA` |+| SQLite | `load_extension(…)`, `ATTACH DATABASE` |+| SQL Server | `xp_cmdshell`, `sp_OACreate`, `OPENROWSET`, `BULK INSERT` |++To enable database writes from the agent, extend the allowlist explicitly+rather than disabling all checks:++```python+config = SQLChatAgentConfig(+ database_uri="postgresql://example.db",+ allowed_statement_types=["SELECT", "INSERT", "UPDATE", "DELETE"],+)+```++If you need DDL or other operations the allowlist doesn't cover, the+recommended approach is still to keep `allow_dangerous_operations=False`+and grant the DB role exactly the privileges you need, rather than relying+on the agent to filter SQL.+ ## Putting it all together In the code below, we will allow the agent to generate the context descriptions
Documents that generated SQL runs arrive through an allowlist plus dangerous-pattern screening, and tells operators to pair it with least-privilege roles because stored text can steer the agent.
@@ -112,30 +112,41 @@ # sqlglot-based statement-type allowlist. _DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [ # PostgreSQL: COPY ... FROM/TO PROGRAM executes shell commands as the DB- # server OS user. This is the primitive used in CVE-2026-25879.+ # server OS user. re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),- # PostgreSQL server-side filesystem access
Show 45 hidden lines
- re.compile(r"\bpg_read_server_files?\b", re.IGNORECASE),- re.compile(r"\bpg_read_binary_file\b", re.IGNORECASE),- re.compile(r"\bpg_ls_dir\b", re.IGNORECASE),+ # PostgreSQL server-side filesystem / log / WAL access. Match the whole+ # pg_read*, pg_stat*, pg_ls*, pg_current_logfile family rather than naming+ # individual functions: near-name functions (pg_read_file vs+ # pg_read_server_file, pg_ls_logdir vs pg_ls_dir, pg_stat_file, ...) all+ # yield the same file/metadata disclosure primitive.+ re.compile(+ r"\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\s*\(", re.IGNORECASE+ ), re.compile(r"\blo_(import|export)\b", re.IGNORECASE), # MySQL/MariaDB filesystem re.compile(r"\binto\s+(outfile|dumpfile)\b", re.IGNORECASE), re.compile(r"\bload_file\s*\(", re.IGNORECASE), re.compile(r"\bload\s+data\b", re.IGNORECASE), # SQLite: load_extension enables loading arbitrary shared objects;- # ATTACH DATABASE can read/write arbitrary files.+ # ATTACH can read/write arbitrary files. The DATABASE keyword is optional+ # per the SQLite grammar (ATTACH [DATABASE] expr AS schema-name), so match+ # both forms. re.compile(r"\bload_extension\s*\(", re.IGNORECASE),- re.compile(r"\battach\s+database\b", re.IGNORECASE),- # SQL Server: command execution and OLE automation+ re.compile(r"\battach\b(\s+database)?\s+['\"\w]", re.IGNORECASE),+ # SQL Server: command execution, OLE automation, and ad-hoc external data+ # sources (OPENDATASOURCE is the connection-string counterpart of+ # OPENROWSET; both can read remote/UNC files). re.compile(r"\bxp_cmdshell\b", re.IGNORECASE), re.compile(r"\bsp_oacreate\b", re.IGNORECASE), re.compile(r"\bsp_oamethod\b", re.IGNORECASE),- re.compile(r"\bopenrowset\b", re.IGNORECASE),+ re.compile(r"\b(openrowset|opendatasource)\b", re.IGNORECASE), re.compile(r"\bbulk\s+insert\b", re.IGNORECASE),- # Generic: stored-program creation and procedural language extensions+ # Generic: stored-program creation and procedural language extensions.+ # LANGUAGE / RULE / EVENT TRIGGER / FOREIGN TABLE are additional PostgreSQL+ # primitives that can register attacker-controlled code. re.compile(- r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger)\b", re.IGNORECASE+ r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger|language|rule|event\s+trigger|foreign\s+table)\b",+ re.IGNORECASE, ), re.compile(r"\bcreate\s+extension\b", re.IGNORECASE), ]
Swaps named file functions for a family wildcard, accepts SQLite attach without its optional keyword, and extends the banned forms to external providers and additional registration statements.
+ # pg_current_logfile family yields the same file/metadata disclosure+ # primitive as pg_read_server_file but uses different function names.+ "SELECT pg_read_file('postgresql.conf')",+ "SELECT pg_read_file('/etc/passwd')",+ "SELECT pg_stat_file('postgresql.conf')",+ "SELECT pg_ls_logdir()",+ "SELECT pg_ls_waldir()",+ "SELECT pg_ls_tmpdir()",+ "SELECT pg_ls_archive_statusdir()",+ "SELECT pg_current_logfile()",+ # SQLite: the DATABASE keyword is optional in the ATTACH grammar.+ "ATTACH '/etc/passwd' AS p",+ # MSSQL: OPENDATASOURCE is the connection-string counterpart of+ # OPENROWSET and can read remote/UNC files.+ "SELECT * FROM OPENDATASOURCE('SQLNCLI11', 'Server=remote').db.sys.tables",+ ],+)+def test_dangerous_pg_file_family_blocked(session, query):+ agent = _make_agent(session)+ rejection = agent._validate_query(query)+ assert rejection is not None+ assert "REJECTED" in rejection+++def test_benign_pg_functions_not_blocked(session):+ """Non-disclosure pg_* functions must remain allowed (no over-match)."""+ agent = _make_agent(session)+ assert agent._validate_query("SELECT pg_typeof(1)") is None+ assert agent._validate_query("SELECT pg_backend_pid()") is None++ def test_multi_statement_with_buried_drop_blocked(session): agent = _make_agent(session) rejection = agent._validate_query("SELECT 1; DROP TABLE items")
Extends coverage to the file-read, stat, directory, and logfile procedures, a bare SQLite attach, and an external provider query, and asserts benign backend functions stay permitted.