February 13, 2025·6 min read·engineeringpostgresqlsecurityencoding

CVE-2025-1094 exposes the parser between libpq and the server

PostgreSQL's February update fixes quoting of invalidly encoded input. The vulnerability depends on intermediate processing, so the client library and psql workflow belong in the remediation.

A string escaped for PostgreSQL can encounter another parser before the server receives it. That is the important detail in CVE-2025-1094. An application can quote an untrusted value with libpq, build a command, and pass that command through psql. The server's eventual rejection of invalid encoding comes too late if the intermediate program has already interpreted part of the input as an instruction. The February 13 release announcement identifies that workflow as a condition of the vulnerability.

The fixed supported releases are PostgreSQL 17.3, 16.7, 15.11, 14.16 and 13.19. The announcement credits Stephen Fewer of Rapid7 with reporting the issue. A description that stops at SQL injection in PostgreSQL misses where the relevant interpretation happens and can send an operator toward an incomplete inventory of affected software.

I still default to PostgreSQL for relational systems after decades of production use. Its predictable semantics are part of that preference. This defect is interesting because the server's own encoding validation remains relevant, yet a surrounding client workflow changes whether that validation can protect the operation.

The advisory describes two affected paths

The project advisory names PQescapeLiteral(), PQescapeIdentifier(), PQescapeString() and PQescapeStringConn(). An input provider can exploit certain uses of their output when the application constructs input for psql. The affected boundary is therefore wider than a particular application query and narrower than every use of an escaping function.

The advisory separately describes command-line utility programs. In that path, attacker-controlled arguments can cause SQL injection when client_encoding is BIG5 and server_encoding is EUC_TW or MULE_INTERNAL. Those encoding conditions belong to that utility scenario. Applying them as a universal prerequisite would incorrectly narrow the other path.

This distinction changes how the finding should be communicated. A report needs to identify whether an application generates input for psql, invokes an affected utility with untrusted arguments, or sends commands directly to the server. Those are questions about actual execution paths. A database inventory containing only server versions cannot answer them.

Invalid bytes reach a client-side interpreter

The 17.3 release notes explain that sending the quoted result directly to the server does not present this hazard because the server checks encoding. Intermediate client code can interpret malformed bytes differently. The fix makes libpq's quoting functions validate input encoding completely and report an error when their interface permits it.

Some older interfaces cannot return an error through a suitable convention. For those functions, PostgreSQL adjusts the generated string so that the server will reject its encoding and intermediate processing will not mistake embedded bytes for quoting syntax. That preserves failure as the outcome even where the API cannot directly express it.

The notes qualify this protection with an agreement requirement. The quoting function, intermediate processing and server must use the same character encoding. An application cannot treat a quoted byte string as independent of the encoding under which it was produced. This is a specific compatibility condition on the fix, rather than a general recommendation to sanitize text again.

Why psql changes the consequences

The psql manual documents a language around SQL. Unquoted backslash commands execute in the client, and \! runs an operating-system shell command. These capabilities are useful in an interactive administration tool. They also mean that input to the tool has effects beyond the SQL statements eventually submitted to a database connection.

The client has its own variable substitution and quoting rules. Consequently, a generated input file is a program for that client. Calling it SQL does not remove those rules. The relevant question is whether an untrusted value can change how the client divides data from instructions before database execution begins.

This explains why database permissions alone do not describe the full consequence of this workflow. A shell command launched by a client executes in the client's environment. The operator must account for the authority of the process reading the generated input, as well as the database role to which that process connects. That conclusion follows from the documented client behavior, without assuming a particular deployment or incident.

The research came from another product's exploit chain

Fewer's Rapid7 disclosure says the team discovered the problem while researching CVE-2024-12356 in BeyondTrust products. In the scenarios Rapid7 tested, successful exploitation of that vulnerability also required the PostgreSQL flaw to achieve remote code execution. BeyondTrust's December patch blocked the tested chain without repairing the underlying PostgreSQL defect.

Rapid7 contacted the PostgreSQL security team on January 27. The project confirmed the finding on January 29, and coordinated disclosure occurred on February 13. These dates describe the disclosed investigation. They do not establish that every PostgreSQL installation was exploited or that every application using libpq exposes the same path.

The research gives a concrete reason to examine utility composition. One product can prevent an exploit through an application-specific change while a reusable component still contains the defect. Conversely, updating that component does not prove that every other weakness in the product has disappeared. The two fixes address different parts of the reported chain.

Encoding is connection state

PostgreSQL's character-set documentation describes automatic conversion between client and server encodings. The database's storage encoding and the client's encoding need not match. The client setting can come from configuration, PGCLIENTENCODING, or commands that change it after connecting. A review based only on a database's declared encoding can therefore miss the setting used by the client.

The documented inspection commands make both values visible in a session:

SHOW client_encoding;
SHOW server_encoding;

These commands establish session facts, not a general claim about all applications connecting to the database. A separate tool may select another encoding, and a program that quotes text before handing it to another process introduces another place where interpretation must agree. That is why the release notes' encoding condition applies to the complete path. Recording only the final connection settings leaves the interpretation used during earlier command construction unspecified, which is exactly the assumption this defect makes consequential.

I value explicit boundaries in database tooling. Here, naming the encoding at each processing step is more useful than describing a string as already escaped. The latter omits the assumptions that make the escaping meaningful to its next consumer.

Keep parameter values outside generated command text

The libpq command-execution reference documents PQexecParams(), which sends parameter values separately from command text. SQL refers to them as $1, $2 and so on. This avoids inserting a value into a constructed SQL literal, and the interface permits only one nonempty SQL command per call.

That is a different construction from generating a script for psql. When a program controls the database connection, passing values through the parameter interface removes the need to create SQL literal syntax for those values. It does not turn arbitrary table names into values, and it does not describe how to make a whole client script safe. The execution model must match the task.

The same reference distinguishes literal escaping from identifier escaping and documents the older connection-free PQescapeString() interface as deprecated. Those distinctions predate this security update. They explain why replacing one helper name mechanically is an incomplete response when the broader workflow still assumes that every consumer interprets the resulting bytes identically.

The library in the running application matters

The libpq miscellaneous-function reference provides PQlibVersion() to report the library actually in use. For modern releases, it multiplies the major version by 10000 and adds the minor version. This observes a client-library version independently of a query against a database server.

That separation is operationally useful for this vulnerability. Updating a server does not establish which library an application loads or which executable a job invokes. The release announcement's normal minor-update procedure addresses PostgreSQL binaries without a dump and reload, but the affected client paths still need their own component inventory. An application using its own packaged library needs evidence about that copy.

The February fix therefore has a concrete completion condition beyond a healthy server connection. The relevant quoting implementation must contain the correction, and its encoding assumptions must agree with every intermediate consumer. Where application code generates psql input, the review must include that client's instruction language and the authority of the process interpreting it.