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.
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_len` variable of type `size_t` supports a large range of values and contains the length of the entire string being processed: 2_147_483_648.
– The `num_quotes` variable, which is of type `signed 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.
**Background** `malloc` (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 `PDOPgsql::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**
“`
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 `PDOPgsql::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 `PDOPgsql::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.
