PYSEC-2026-2577
Vulnerability from pysec - Published: 2026-07-13 15:46 - Updated: 2026-07-13 16:04SQLChatAgent _validate_query dangerous-pattern regex is bypassable via quoted/commented/qualified function names
Summary
The SQLChatAgent SQL-injection mitigation, with default allow_dangerous_operations=False, combines a raw-text regex blocklist (_DANGEROUS_SQL_PATTERNS) with a sqlglot SELECT-only statement allowlist. The blocklist entries that target callable functions require the function name to be immediately followed by \s*\(.
PostgreSQL accepts the same call with the name separated from ( by a quoted identifier, an inline comment, or schema qualification. These forms evade the regex, still parse as SELECT, and execute the same PostgreSQL function. This restores the pg_read_file server-side file-read primitive that the prior CVE-2026-25879 / GHSA-pmch-g965-grmr fix was meant to block: the parent advisory fixed a missing pg_read_file blocklist entry, while this report shows that the added regex is bypassable.
Affected Code
Tested against current main commit:
6e8e7b2bb23ec04c1c25be479f16b8cc9a4f8796
The current source still contains:
re.compile(r"\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\s*\(", re.IGNORECASE)
_validate_query checks the raw query against _DANGEROUS_SQL_PATTERNS, then parses with sqlglot and allows SELECT statements. The dangerous-call check is raw text, not normalized AST function-name matching.
Root Cause
The current mitigation treats dangerous PostgreSQL function calls as a raw-text regex problem. The regex requires the pg_... function token to be followed directly by optional whitespace and (, but PostgreSQL accepts equivalent calls through quoted identifiers, comments, and schema-qualified names. Because _validate_query only uses sqlglot to enforce the top-level statement type, those normalized function names are never checked after parsing.
Auth Boundary
The boundary is the default SQLChatAgent safety policy between attacker-influenced SQL generation and database operations that can read server-side files. With allow_dangerous_operations=False, a user or prompt that influences generated SQL should not be able to bypass the guard and execute PostgreSQL file-read functions such as pg_read_file.
This is not a new unauthenticated endpoint or product-wide SQL injection; it applies when untrusted user content can influence SQLChatAgent's generated SQL.
Reproduction
The local harness uses the current sql_chat_agent.py, extracts the real shipped dangerous regex list, validates the queries with real sqlglot==30.8.0, then executes the accepted bypasses against a local throwaway PostgreSQL 16 container.
Transcript excerpt:
CONTROL "SELECT pg_read_file('/etc/passwd')" -> REJECTED: matches '\\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\\s*\\('
BYPASS 'SELECT "pg_read_file"(\'/etc/passwd\')' -> ALLOWED (validator returned None -> would execute)
BYPASS "SELECT pg_read_file/**/('/etc/passwd')" -> ALLOWED (validator returned None -> would execute)
BYPASS 'SELECT pg_catalog."pg_read_file"(\'/etc/passwd\')' -> ALLOWED (validator returned None -> would execute)
=== Part B: real PostgreSQL execution of the bypass ===
connected; is_superuser=t
executed bypass 'SELECT "pg_read_file"(\'<file>\')' -> file contents returned: 'LANGROID_SAFE_MARKER_...'
executed bypass "SELECT pg_read_file/**/('<file>')" -> file contents returned: 'LANGROID_SAFE_MARKER_...'
executed bypass 'SELECT pg_catalog."pg_read_file"(\'<file>\')' -> file contents returned: 'LANGROID_SAFE_MARKER_...'
RESULT: VULNERABLE
The control query is blocked by the current regex, while all three equivalent PostgreSQL forms are allowed by the validator and return the mounted proof file contents from a real PostgreSQL server. The LANGROID_SAFE_MARKER_... value is a harmless marker generated inside the throwaway local container for this proof.
Impact
On a deployment using SQLChatAgent against PostgreSQL with a role able to call pg_read_file (superuser, or a role granted pg_read_server_files), an attacker who can influence LLM-generated SQL can coerce the agent into emitting one of the obfuscated queries and read files accessible to the PostgreSQL server process through pg_read_file.
This is the same impact and precondition shape as the published pg_read_file advisory, but it targets the bypassability of the current regex-based fix rather than the pre-fix absence of a pg_read_file block.
Severity: High by parity with the published parent advisory; not Critical. CWE-184 leading to server-side file read.
Suggested Fix
Do not rely on raw-text regex matching for dangerous-call detection. After the existing sqlglot parse, walk the AST and reject any function invocation whose normalized, unquoted, schema-stripped, case-folded name is in a dangerous set such as pg_read_file, pg_read_binary_file, pg_ls_dir, pg_stat_file, lo_import, lo_export, load_file, or load_extension.
Also recommend running SQLChatAgent with a least-privilege database role that lacks pg_read_server_files.
| Name | purl | langroid | pkg:pypi/langroid |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "langroid",
"purl": "pkg:pypi/langroid"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.65.1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1.100",
"0.1.101",
"0.1.102",
"0.1.103",
"0.1.104",
"0.1.105",
"0.1.106",
"0.1.107",
"0.1.108",
"0.1.109",
"0.1.11",
"0.1.110",
"0.1.111",
"0.1.112",
"0.1.113",
"0.1.114",
"0.1.117",
"0.1.118",
"0.1.119",
"0.1.12",
"0.1.120",
"0.1.121",
"0.1.122",
"0.1.123",
"0.1.124",
"0.1.125",
"0.1.126",
"0.1.127",
"0.1.128",
"0.1.129",
"0.1.13",
"0.1.130",
"0.1.131",
"0.1.132",
"0.1.133",
"0.1.134",
"0.1.135",
"0.1.136",
"0.1.137",
"0.1.138",
"0.1.139",
"0.1.140",
"0.1.141",
"0.1.142",
"0.1.143",
"0.1.144",
"0.1.145",
"0.1.147",
"0.1.148",
"0.1.149",
"0.1.15",
"0.1.150",
"0.1.151",
"0.1.152",
"0.1.153",
"0.1.154",
"0.1.155",
"0.1.156",
"0.1.157",
"0.1.158",
"0.1.159",
"0.1.160",
"0.1.161",
"0.1.162",
"0.1.163",
"0.1.164",
"0.1.165",
"0.1.166",
"0.1.167",
"0.1.168",
"0.1.169",
"0.1.17",
"0.1.170",
"0.1.171",
"0.1.172",
"0.1.173",
"0.1.174",
"0.1.175",
"0.1.176",
"0.1.177",
"0.1.178",
"0.1.179",
"0.1.18",
"0.1.181",
"0.1.182",
"0.1.183",
"0.1.184",
"0.1.185",
"0.1.186",
"0.1.187",
"0.1.188",
"0.1.189",
"0.1.19",
"0.1.190",
"0.1.191",
"0.1.192",
"0.1.193",
"0.1.194",
"0.1.195",
"0.1.196",
"0.1.197",
"0.1.198",
"0.1.199",
"0.1.20",
"0.1.200",
"0.1.201",
"0.1.202",
"0.1.203",
"0.1.205",
"0.1.206",
"0.1.207",
"0.1.208",
"0.1.209",
"0.1.21",
"0.1.210",
"0.1.211",
"0.1.212",
"0.1.213",
"0.1.214",
"0.1.215",
"0.1.217",
"0.1.218",
"0.1.219",
"0.1.22",
"0.1.221",
"0.1.222",
"0.1.224",
"0.1.225",
"0.1.226",
"0.1.227",
"0.1.228",
"0.1.229",
"0.1.23",
"0.1.230",
"0.1.231",
"0.1.233",
"0.1.234",
"0.1.235",
"0.1.236",
"0.1.237",
"0.1.238",
"0.1.239",
"0.1.24",
"0.1.240",
"0.1.241",
"0.1.243",
"0.1.244",
"0.1.245",
"0.1.246",
"0.1.247",
"0.1.248",
"0.1.249",
"0.1.25",
"0.1.250",
"0.1.251",
"0.1.252",
"0.1.253",
"0.1.254",
"0.1.256",
"0.1.257",
"0.1.258",
"0.1.26",
"0.1.260",
"0.1.261",
"0.1.262",
"0.1.263",
"0.1.265",
"0.1.27",
"0.1.28",
"0.1.29",
"0.1.30",
"0.1.31",
"0.1.32",
"0.1.33",
"0.1.34",
"0.1.35",
"0.1.36",
"0.1.37",
"0.1.38",
"0.1.39",
"0.1.40",
"0.1.41",
"0.1.42",
"0.1.43",
"0.1.44",
"0.1.46",
"0.1.47",
"0.1.48",
"0.1.49",
"0.1.50",
"0.1.51",
"0.1.52",
"0.1.53",
"0.1.54",
"0.1.55",
"0.1.56",
"0.1.57",
"0.1.58",
"0.1.59",
"0.1.60",
"0.1.61",
"0.1.62",
"0.1.63",
"0.1.64",
"0.1.65",
"0.1.66",
"0.1.67",
"0.1.68",
"0.1.69",
"0.1.72",
"0.1.73",
"0.1.76",
"0.1.77",
"0.1.78",
"0.1.79",
"0.1.8",
"0.1.80",
"0.1.81",
"0.1.83",
"0.1.84",
"0.1.85",
"0.1.86",
"0.1.87",
"0.1.88",
"0.1.89",
"0.1.9",
"0.1.90",
"0.1.91",
"0.1.92",
"0.1.93",
"0.1.94",
"0.1.95",
"0.1.96",
"0.1.97",
"0.1.98",
"0.1.99",
"0.10.0",
"0.10.1",
"0.10.2",
"0.11.0",
"0.12.0",
"0.13.0",
"0.14.0",
"0.15.0",
"0.15.1",
"0.15.2",
"0.16.0",
"0.16.1",
"0.16.2",
"0.16.3",
"0.16.4",
"0.16.5",
"0.16.6",
"0.16.7",
"0.17.0",
"0.17.1",
"0.18.0",
"0.18.1",
"0.18.2",
"0.18.3",
"0.19.0",
"0.19.1",
"0.19.2",
"0.19.3",
"0.19.4",
"0.19.5",
"0.2.0",
"0.2.10",
"0.2.11",
"0.2.12",
"0.2.2",
"0.2.3",
"0.2.4",
"0.2.5",
"0.2.6",
"0.2.7",
"0.2.9",
"0.20.0",
"0.20.1",
"0.21.0",
"0.22.0",
"0.22.1",
"0.22.2",
"0.22.3",
"0.22.4",
"0.22.5",
"0.22.6",
"0.22.7",
"0.23.0",
"0.23.1",
"0.23.2",
"0.23.3",
"0.24.1",
"0.25.0",
"0.26.0",
"0.26.1",
"0.26.2",
"0.27.1",
"0.27.2",
"0.27.3",
"0.27.4",
"0.28.0",
"0.28.1",
"0.28.2",
"0.28.3",
"0.28.4",
"0.28.5",
"0.28.6",
"0.28.7",
"0.29.0",
"0.3.0",
"0.3.1",
"0.30.0",
"0.30.1",
"0.31.0",
"0.31.1",
"0.31.2",
"0.31.3",
"0.32.0",
"0.32.1",
"0.32.2",
"0.33.10",
"0.33.11",
"0.33.12",
"0.33.13",
"0.33.3",
"0.33.4",
"0.33.6",
"0.33.7",
"0.33.8",
"0.33.9",
"0.34.0",
"0.34.1",
"0.35.0",
"0.35.1",
"0.36.0",
"0.36.1",
"0.37.0",
"0.37.1",
"0.37.2",
"0.37.3",
"0.37.4",
"0.37.5",
"0.37.6",
"0.37.7",
"0.38.0",
"0.39.0",
"0.39.1",
"0.39.2",
"0.39.3",
"0.39.4",
"0.39.5",
"0.40.0",
"0.41.0",
"0.41.1",
"0.41.2",
"0.41.3",
"0.41.4",
"0.41.5",
"0.42.0",
"0.42.1",
"0.42.10",
"0.42.2",
"0.42.3",
"0.42.4",
"0.42.5",
"0.42.6",
"0.42.7",
"0.42.8",
"0.42.9",
"0.43.0",
"0.43.1",
"0.44.0",
"0.45.0",
"0.45.1",
"0.45.10",
"0.45.2",
"0.45.3",
"0.45.4",
"0.45.5",
"0.45.6",
"0.45.7",
"0.45.8",
"0.46.0",
"0.47.0",
"0.47.1",
"0.47.2",
"0.48.0",
"0.48.1",
"0.48.2",
"0.48.3",
"0.49.0",
"0.49.1",
"0.5.0",
"0.5.1",
"0.50.0",
"0.50.1",
"0.50.10",
"0.50.11",
"0.50.12",
"0.50.2",
"0.50.3",
"0.50.4",
"0.50.5",
"0.50.6",
"0.50.7",
"0.50.8",
"0.50.9",
"0.51.0",
"0.51.1",
"0.51.2",
"0.52.0",
"0.52.1",
"0.52.2",
"0.52.3",
"0.52.4",
"0.52.5",
"0.52.6",
"0.52.7",
"0.52.8",
"0.52.9",
"0.53.0",
"0.53.1",
"0.53.10",
"0.53.11",
"0.53.12",
"0.53.13",
"0.53.14",
"0.53.15",
"0.53.16",
"0.53.2",
"0.53.4",
"0.53.5",
"0.53.6",
"0.53.7",
"0.53.8",
"0.54.0",
"0.54.1",
"0.54.2",
"0.55.0",
"0.55.1",
"0.56.0",
"0.56.1",
"0.56.10",
"0.56.11",
"0.56.12",
"0.56.13",
"0.56.14",
"0.56.15",
"0.56.16",
"0.56.17",
"0.56.18",
"0.56.19",
"0.56.2",
"0.56.3",
"0.56.4",
"0.56.5",
"0.56.6",
"0.56.7",
"0.56.8",
"0.56.9",
"0.57.0",
"0.58.0",
"0.58.1",
"0.58.2",
"0.58.3",
"0.59.0",
"0.59.0b1",
"0.59.0b2",
"0.59.0b3",
"0.59.1",
"0.59.10",
"0.59.11",
"0.59.12",
"0.59.13",
"0.59.14",
"0.59.15",
"0.59.16",
"0.59.17",
"0.59.18",
"0.59.19",
"0.59.2",
"0.59.20",
"0.59.21",
"0.59.22",
"0.59.23",
"0.59.24",
"0.59.25",
"0.59.26",
"0.59.27",
"0.59.28",
"0.59.29",
"0.59.3",
"0.59.30",
"0.59.31",
"0.59.32",
"0.59.33",
"0.59.34",
"0.59.35",
"0.59.36",
"0.59.37",
"0.59.38",
"0.59.39",
"0.59.4",
"0.59.5",
"0.59.6",
"0.59.7",
"0.59.8",
"0.59.9",
"0.6.0",
"0.6.1",
"0.6.3",
"0.6.4",
"0.6.5",
"0.6.6",
"0.6.7",
"0.60.0",
"0.60.1",
"0.60.2",
"0.60.3",
"0.61.0",
"0.61.1",
"0.62.0",
"0.63.0",
"0.64.0",
"0.65.0",
"0.8.0",
"0.9.0",
"0.9.1",
"0.9.2",
"0.9.3",
"0.9.4",
"0.9.5"
]
}
],
"aliases": [
"CVE-2026-54760",
"GHSA-6xc5-4r68-67fc"
],
"details": "# SQLChatAgent `_validate_query` dangerous-pattern regex is bypassable via quoted/commented/qualified function names\n\n## Summary\n\nThe `SQLChatAgent` SQL-injection mitigation, with default `allow_dangerous_operations=False`, combines a raw-text regex blocklist (`_DANGEROUS_SQL_PATTERNS`) with a `sqlglot` SELECT-only statement allowlist. The blocklist entries that target callable functions require the function name to be immediately followed by `\\s*\\(`.\n\nPostgreSQL accepts the same call with the name separated from `(` by a quoted identifier, an inline comment, or schema qualification. These forms evade the regex, still parse as `SELECT`, and execute the same PostgreSQL function. This restores the `pg_read_file` server-side file-read primitive that the prior CVE-2026-25879 / GHSA-pmch-g965-grmr fix was meant to block: the parent advisory fixed a missing `pg_read_file` blocklist entry, while this report shows that the added regex is bypassable.\n\n## Affected Code\n\nTested against current `main` commit:\n\n`6e8e7b2bb23ec04c1c25be479f16b8cc9a4f8796`\n\nThe current source still contains:\n\n```python\nre.compile(r\"\\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\\s*\\(\", re.IGNORECASE)\n```\n\n`_validate_query` checks the raw query against `_DANGEROUS_SQL_PATTERNS`, then parses with `sqlglot` and allows `SELECT` statements. The dangerous-call check is raw text, not normalized AST function-name matching.\n\n## Root Cause\n\nThe current mitigation treats dangerous PostgreSQL function calls as a raw-text regex problem. The regex requires the `pg_...` function token to be followed directly by optional whitespace and `(`, but PostgreSQL accepts equivalent calls through quoted identifiers, comments, and schema-qualified names. Because `_validate_query` only uses `sqlglot` to enforce the top-level statement type, those normalized function names are never checked after parsing.\n\n## Auth Boundary\n\nThe boundary is the default `SQLChatAgent` safety policy between attacker-influenced SQL generation and database operations that can read server-side files. With `allow_dangerous_operations=False`, a user or prompt that influences generated SQL should not be able to bypass the guard and execute PostgreSQL file-read functions such as `pg_read_file`.\n\nThis is not a new unauthenticated endpoint or product-wide SQL injection; it applies when untrusted user content can influence SQLChatAgent\u0027s generated SQL.\n\n## Reproduction\n\nThe local harness uses the current `sql_chat_agent.py`, extracts the real shipped dangerous regex list, validates the queries with real `sqlglot==30.8.0`, then executes the accepted bypasses against a local throwaway PostgreSQL 16 container.\n\nTranscript excerpt:\n\n```text\nCONTROL \"SELECT pg_read_file(\u0027/etc/passwd\u0027)\" -\u003e REJECTED: matches \u0027\\\\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\\\\s*\\\\(\u0027\nBYPASS \u0027SELECT \"pg_read_file\"(\\\u0027/etc/passwd\\\u0027)\u0027 -\u003e ALLOWED (validator returned None -\u003e would execute)\nBYPASS \"SELECT pg_read_file/**/(\u0027/etc/passwd\u0027)\" -\u003e ALLOWED (validator returned None -\u003e would execute)\nBYPASS \u0027SELECT pg_catalog.\"pg_read_file\"(\\\u0027/etc/passwd\\\u0027)\u0027 -\u003e ALLOWED (validator returned None -\u003e would execute)\n\n=== Part B: real PostgreSQL execution of the bypass ===\nconnected; is_superuser=t\n executed bypass \u0027SELECT \"pg_read_file\"(\\\u0027\u003cfile\u003e\\\u0027)\u0027 -\u003e file contents returned: \u0027LANGROID_SAFE_MARKER_...\u0027\n executed bypass \"SELECT pg_read_file/**/(\u0027\u003cfile\u003e\u0027)\" -\u003e file contents returned: \u0027LANGROID_SAFE_MARKER_...\u0027\n executed bypass \u0027SELECT pg_catalog.\"pg_read_file\"(\\\u0027\u003cfile\u003e\\\u0027)\u0027 -\u003e file contents returned: \u0027LANGROID_SAFE_MARKER_...\u0027\n\nRESULT: VULNERABLE\n```\n\nThe control query is blocked by the current regex, while all three equivalent PostgreSQL forms are allowed by the validator and return the mounted proof file contents from a real PostgreSQL server. The `LANGROID_SAFE_MARKER_...` value is a harmless marker generated inside the throwaway local container for this proof.\n\n## Impact\n\nOn a deployment using `SQLChatAgent` against PostgreSQL with a role able to call `pg_read_file` (superuser, or a role granted `pg_read_server_files`), an attacker who can influence LLM-generated SQL can coerce the agent into emitting one of the obfuscated queries and read files accessible to the PostgreSQL server process through `pg_read_file`.\n\nThis is the same impact and precondition shape as the published `pg_read_file` advisory, but it targets the bypassability of the current regex-based fix rather than the pre-fix absence of a `pg_read_file` block.\n\nSeverity: High by parity with the published parent advisory; not Critical. CWE-184 leading to server-side file read.\n\n## Suggested Fix\n\nDo not rely on raw-text regex matching for dangerous-call detection. After the existing `sqlglot` parse, walk the AST and reject any function invocation whose normalized, unquoted, schema-stripped, case-folded name is in a dangerous set such as `pg_read_file`, `pg_read_binary_file`, `pg_ls_dir`, `pg_stat_file`, `lo_import`, `lo_export`, `load_file`, or `load_extension`.\n\nAlso recommend running SQLChatAgent with a least-privilege database role that lacks `pg_read_server_files`.",
"id": "PYSEC-2026-2577",
"modified": "2026-07-13T16:04:33.415429Z",
"published": "2026-07-13T15:46:28.525610Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/langroid/langroid/security/advisories/GHSA-6xc5-4r68-67fc"
},
{
"type": "PACKAGE",
"url": "https://github.com/langroid/langroid"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/langroid"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-6xc5-4r68-67fc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54760"
}
],
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Langroid: SQLChatAgent dangerous-function blocklist can be bypassed with quoted or schema-qualified pg_read_file calls"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.