FlawOpen

flawopen.com/Incidents/postgresql-psql-injection-beyondtrust-treasury-december-2024

● PostgreSQL / BeyondTrust · CVE-2025-1094 · December 2024 – February 2025 · CVSS 8.1 · High
FlawOpen Security Research

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.

💡 Plain English Explainer (ELI5)

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

  • CWE reference for the weakness described in this guide: CWE-89 CWE-149

Incident Timeline

2024-12-16

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.

2024-12-19

CISA adds CVE-2024-12356 to its Known Exploited Vulnerabilities catalog.

2025-02-13

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

Step 1

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.

Step 2

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.

Step 3

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.

Step 4

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.

UNPATCHED FLAW
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
HARDENED SECURE PATCH
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

Sources