flawopen.com/Incidents/postgresql-psql-injection-beyondtrust-treasury-december-2024
CVE-2025-1094: PostgreSQL psql SQL injection
Invalid UTF-8 bypassed libpq quoting when apps used escaped text to build psql input, enabling SQL or \! injection. Fixed Feb. 13, 2025.
Imagine a translator whose job is to wrap everything you say in quotation marks so it can never be mistaken for a command. You discover that if you hand them a word in an alphabet they do not recognise, they get confused and drop one of the quotation marks. What you said now escapes its quotes and is read as an instruction, even though the translator did exactly what it was designed to do for every input anyone had tested.
Core Concepts & Subsystem Terms
PQescapeLiteral- A libpq function that wraps a value in quotes and escapes it so it can be pasted into SQL text. PQescapeIdentifier, PQescapeString and PQescapeStringConn were affected too.
Invalid UTF-8- A byte sequence that is not well-formed in the declared encoding. The escaping code and psql disagreed about how such bytes consume the characters after them.
psql meta-command- A backslash command handled by the psql client rather than the server. \! runs the rest of the line as an operating-system shell command.
CVE-2024-12356- A critical command-injection flaw in BeyondTrust Privileged Remote Access and Remote Support. In Rapid7's tests, reaching code execution through it required
CVE-2025-1094. Bound parameter- A value sent to the server separately from the SQL text, so no quoting or escaping decision is ever made on it.
Framework mappings
Incident Timeline
BeyondTrust publishes advisory BT24-10 for CVE-2024-12356 after investigating a compromise of some Remote Support SaaS instances; the US Treasury later reported that this incident reached its systems.
CISA adds CVE-2024-12356 to its Known Exploited Vulnerabilities catalog.
PostgreSQL releases 17.3, 16.7, 15.11, 14.16 and 13.19 fixing CVE-2025-1094, and Rapid7 publishes its analysis.
Step-by-Step Attack Flow
Input reaches an escaping function
An application takes attacker-influenced text and passes it to PQescapeLiteral or a related libpq function to build a SQL statement.
Invalid UTF-8 hides a quote
A crafted invalid multi-byte sequence placed before a single quote caused the escaping routine to leave that quote unescaped, per the PostgreSQL advisory.
psql ends the literal early
When the statement is fed to psql, the unescaped quote closes the string literal and the rest of the input is parsed as SQL or as psql commands.
A meta-command runs a shell
Rapid7 showed that the injected text can include the \! meta-command, which psql executes as an operating-system command on the machine running psql.
Source Code: Flaw vs. Secure Implementation
Illustrative Python using psycopg 3, not BeyondTrust's code. The vulnerable version reproduces the risky pattern the advisory names: escape a value with libpq, splice it into SQL text and pipe that text to psql. The hardened version removes both halves: it rejects malformed UTF-8 before any processing, and sends the value as a bound parameter through a real driver, so there is no escaping step to fool and no psql meta-command parser in the path. Upgrading libpq still matters for any code that keeps escaping.
import subprocess
import psycopg
def lookup_user(conn: psycopg.Connection, name: bytes) -> bytes:
# libpq's PQescapeLiteral (before 17.3/16.7/15.11/14.16/13.19) did not
# neutralise a quote hidden behind an invalid UTF-8 byte sequence.
literal = conn.pgconn.escape_literal(name)
script = b"SELECT id FROM users WHERE name = " + literal + b";\n"
# psql runs its input as a script, including meta-commands such as \!
result = subprocess.run(["psql", "-At", "-d", "app"], input=script,
capture_output=True, check=True)
return result.stdout
import psycopg
MAX_NAME_CHARS = 128
def lookup_user(conn: psycopg.Connection, name: bytes) -> list[int]:
try:
text = name.decode("utf-8") # reject malformed input at the boundary
except UnicodeDecodeError:
raise ValueError("name is not valid UTF-8") from None
if len(text) > MAX_NAME_CHARS:
raise ValueError("name too long")
with conn.cursor() as cur:
# The value is sent as a bound parameter, separate from the SQL text,
# and no psql process (and so no meta-command parser) is involved.
cur.execute("SELECT id FROM users WHERE name = %s", (text,))
return [row[0] for row in cur.fetchall()]
Engineering & System Hardening Checklist
- ✓Search the codebase for subprocess calls to psql whose input is built from application data, and replace them with a database driver using bound parameters; add a CI rule that fails on new ones.
- ✓Reject input that is not well-formed in its declared encoding at the boundary, before it reaches any parser, escaper or database call; add a test that sends b'\xc0\'' and expects a validation error.
- ✓Upgrade libpq and client tools, not only the server:
CVE-2025-1094is fixed in PostgreSQL 17.3, 16.7, 15.11, 14.16 and 13.19, and client packages on application hosts must be updated too. - ✓Where escaping cannot be removed yet, add a regression test that feeds invalid UTF-8 followed by a quote to the escaping path and asserts the result is rejected or safely quoted.
- ✓Treat privileged remote-access tools as part of your attack surface: restrict their management interfaces, patch them on vendor notice and monitor their API keys for use from unexpected locations.