
Proof-of-concept demonstrating CVE-2026-17351 SQL injection bypass in pgAdmin 4's AI Assistant via sqlparse/PostgreSQL lexer differential, including indirect prompt injection delivery.
PoC for CVE-2026-17351, a critical (CVSS 9.0) SQL injection bypass in pgAdmin 4's AI Assistant.
pgAdmin 4 versions 9.13 through 9.16 use Python's sqlparse library to validate that LLM-generated SQL queries are single, read-only statements. Under PostgreSQL's standard_conforming_strings = on (the default since PostgreSQL 9.1), sqlparse and PostgreSQL disagree about how backslashes inside string literals are handled. This allows an attacker to craft a payload that passes sqlparse's validation as a single SELECT but executes as four statements in PostgreSQL — including a COMMIT that ends the read-only transaction and subsequent write statements.
SELECT '\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --'
SELECT with a big string literal → validation passesSELECT '\' + COMMIT + CREATE TABLE pwn(x int) + SELECT 1 --'The payload is delivered via indirect prompt injection: the attacker plants it inside a database object (column comment, row value, view definition) that the AI Assistant reads. When a legitimate user asks a question, the LLM reads the poisoned data and emits it as an execute_sql_query tool call.
| File | Description |
|---|---|
poc.py | Demonstrates the lexer differential: sqlparse validation passes, simple query protocol executes multiple statements, extended query protocol blocks it |
prompt_injection_demo.py | Simulates the full attack chain: payload planted as a column comment → simulated LLM reads it → validation passes → execution |
pip install psycopg[binary] sqlparse
You also need a running PostgreSQL instance (14+):
docker run -d --name pg-poc \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=testdb \
-p 5433:5432 \
postgres:18
# Main PoC — lexer differential demonstration
python3 poc.py
# Prompt injection delivery demo
python3 prompt_injection_demo.py
# Custom connection parameters
python3 poc.py --host 10.0.0.5 --port 5432 --user postgres --password mypw
The fix forces psycopg3's extended query protocol by:
conn.prepare_threshold = 0 on the LLM's dedicated connectionprepare=True to cursor.execute()This makes PostgreSQL's own Parse step the authority on statement boundaries — it structurally rejects multi-statement text regardless of how any client-side lexer classifies it.
This PoC is for educational and authorized testing purposes only.