# Langroid: SQLChatAgent _validate_query blocklist misses pg_read_file family enabling arbitrary file read

Langroid SQLChatAgent's regex blocklist misses pg_read_file/pg_stat_file/pg_ls_* family, letting prompt-injected SELECT queries read arbitrary files from the PostgreSQL host.

- Payout: Research
- Severity: high
- Program: Langroid
- Found by: tonghuaroot
- Disclosed: 2026-07-02
- CVE: CVE-2026-50180
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-pmch-g965-grmr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pmch-g965-grmr
- Tags: area:ai domain:ai domain:web3 impact:data-leak kind:research platform:ghsa sev:high tech:llm tech:python technique:incomplete-fix technique:source-review vuln:access-control vuln:path-traversal vuln:prompt-injection

**Target:** SQLChatAgent _validate_query SQL blocklist / PostgreSQL engine
**Root cause:** The _DANGEROUS_SQL_PATTERNS blocklist enumerates exact function names (pg_read_server_file, pg_ls_dir) instead of the family prefix, so sibling functions like pg_read_file, pg_stat_file, pg_ls_logdir, pg_ls_waldir, pg_current_logfile pass both the SELECT-only allowlist and the regex check.
**Steps:**
1. Install langroid 0.63.0 with sqlglot and sqlalchemy<2.1
2. Load _DANGEROUS_SQL_PATTERNS and _DEFAULT_ALLOWED_STATEMENTS from the shipped sql_chat_agent.py
3. Submit `SELECT pg_read_file('langroid_bypass_victim.txt')` to _validate_query — passes regex and SELECT allowlist
4. Execute the same query via SQLAlchemy engine against PostgreSQL; file contents returned verbatim
5. Repeat with `SELECT pg_stat_file(...)`, `SELECT pg_ls_logdir()`, `SELECT pg_ls_waldir()`, `SELECT pg_current_logfile()`
**How found:** Code review of the blocklist: noticed patterns are exact-token `\b<name>\b` literals and that PostgreSQL ships near-name siblings on the same primitive (pg_read_file vs pg_read_server_file) that no entry matches; verified each is SELECT-shaped so it also survives the statement-type allowlist.
**Exploitation:** Attacker shapes LLM-generated SQL via direct prompt input or prompt-injection in data the agent ingests; the resulting SELECT-shaped pg_read_file call survives _validate_query and reaches session.execute(text(query)) in run_query, returning host file contents to the LLM/attacker. Superuser DB roles allow absolute paths (postgresql.conf, pg_hba.conf, ~/.pgpass, TLS keys).
**Escalation:** MSSQL OPENDATASOURCE (unblocked sibling of OPENROWSET) enables remote/UNC reads and intranet pivot; SQLite `ATTACH '<path>' AS x` bypasses the DATABASE-keyword-requiring regex when ATTACH is in allowed_statement_types.
**Insight:** When a security control enumerates dangerous function names by exact token, enumerate the vendor's whole function family (prefix regex or AST function-name walk) — PostgreSQL's pg_read_*/pg_stat_*/pg_ls_* siblings and MSSQL's OPENDATASOURCE vs OPENROWSET are classic near-name gaps.

## Proof of concept (copied exactly from the original report)

**Bypass payloads list with pg_read_file and pg_ls functions**

```
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()"),
    ]
```

**Install and run commands for PoC reproduction**

```
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
```

