Exploiting CVE-2025-29744: When Prepared Statements Aren't Safe

Table Of Contents
This post analyzes a subtle SQL injection vulnerability — CVE-2025-29744 — in pg-promise, a popular PostgreSQL library for Node.js built on top of node-postgres.
Discovered by the Sonar team, this flaw affects how pg-promise handles certain prepared statements when using the simple query protocol. Under specific conditions, it may allow attackers to inject SQL—even if first-party code uses parameterized queries.
Exploitation requires:
- Use of prepared statements in a particular structure.
- Use of the simple query protocol instead of the extended one.
Vulnerability Details
The vulnerability involved injecting inline comments in a situation where subtraction from a value was possible, such as:
const query = 'UPDATE users SET balance = balance-$1 WHERE id = $2';
await db.none(query, [amount, userId]);
In this JS snippet, $1 is replaced with the value of amount and $2 is replaced with userId in a prepared statement. Assuming $1 is user controlled input, if the user enters negative value say -5 the SQL statement becomes:
UPDATE users SET balance = balance--5 WHERE id = 1
This situation results in inline commenting a part of the statement from the point of user input, which means 5 WHERE id = 1 was commented out and ignored.
Usually prepared statements are considered a secure practice to prevent SQL injection, but different libraries might handle comments differently, just like MySQL requires whitespace after the -- start comment sequence.
Setting up a vulnerable lab; Exploitation; Impact
In practical applications, the requirements for exploiting this CVE are quite stringent. In this section, we’ll explore what conditions are necessary for exploitation, what worked in our testing, and what didn’t.
Setting up the vulnerable application
To test the exploitability of this CVE locally, I built a minimal inventory app where users can input an amount and a username. These inputs are used in a SQL statement that modifies the user’s balance, satisfying the vulnerability prerequisites.
Lab setup:
cvePlayground is a repository of intentionally vulnerable CVE labs with detailed writeups and reproducible environments for hands-on learning and security research.
[FOLDER] This writeup corresponds to the following lab: CVE-2025-29744 — pg-promise SQL Injection
You’ll find setup instructions and exploitation steps inside the linked repository folder.

Following is the snippet where the vulnerability exists, even thought the application uses prepared statements, the application is vulnerable to SQL injection because vulnerable library pg-promise v11.5.4 failed to implement required security standards which leads to commenting the query when a negative integer is entered.

Initial Testing
When submitting the -1 as the input 1 (amount) and cat as the input 2 which is an existing user in the database, the Node.js server throws an error.

The following error is a clear indication that the SQL statement failed due PostgreSQL encountering a syntax error because the query ended unexpectedly, likely due to an improperly handled comment (--).
The query contains:
SELECT * FROM pg_promise_example WHERE result=--1 OR name='cat';
and PostgreSQL interpreted it as:
SELECT * FROM pg_promise_example WHERE result=
resulting an error.
The error confirms that the inline comment sequence is indeed commenting out rest of the query.
Crafting the Impact: using PostgreSQL against PostgreSQL
The official POC mentioned, PostgreSQL supports multi literals, which means a new line character (\n) can be used to pass multiple inputs! When -1 is passed that comments rest of the statement, passing foo \n bar makes the query look like:
SELECT * FROM pg_promise_example WHERE result=--1 OR name='foo
bar';
As a result, when PostgreSQL parses this query, it will ignore the comments and treat bar'; as additional statement.
An attacker can craft malicious query such as:
SELECT * FROM pg_promise_example WHERE result=--1 OR name='foo
INSERT INTO users (id, username, balance) VALUES (1337,"BigCat",13370000)';--';
Exploitation Example
Moving back to our vulnerable application, let’s enumerate version of the database with the above discussed knowledge.

Again, -1 comments the statement, and after cat a new line is treated as multi literal which makes the query look like:
SELECT * FROM pg_promise_example WHERE result=--1 OR name='cat
1 AND 1=0 UNION SELECT 1337, version(); --

From this point on, we have significant control — the possibilities are wide open.
The Patch
To address the vulnerability, the pg-promise team introduced a utility function designed to safely format numeric values before injecting them into SQL queries—especially in contexts involving subtraction.

This utility is responsible for converting numeric values into safe SQL-friendly strings, and here’s how it works:
Ensuring valid input
if (typeof num === 'bigint' || Number.isFinite(num))
This condition ensures that the function only operates on a valid numeric inputs:
typeof num === 'bigint': results to true if the input is a BigInt (example -123n)Number.isFinite(num): results to true if it’s a finite number (notNaN,Infinity, etc.)
To conclude, this block of code only run for valid BigInt or finite numbers.
Conversion to string
const s = num.toString();
Once confirmed as safe, the number is converted to a string for formatting and interpolation.
Safe Formatting: Parentheses
return num < 0 ? `(${s})` : s;
A classic ternary operator that returns:
- If the number is negative, it wraps the value in parentheses. For example,
-100turns into"(-100)".- This prevents PostgreSQL from misinterpreting a double dash (
--) formed during inline subtraction as the start of a comment.
- This prevents PostgreSQL from misinterpreting a double dash (
- If the number is non-negative i.e. positive or zero, it returns the number as a string. For example,
10as a input is turned into"10".
This formatting cleverly avoid accidental Comment injection from input like --5, which would previously result in syntactically invalid or exploitable SQL like:
UPDATE users SET balance = balance--5 WHERE id = 1;
-- becomes → UPDATE users SET balance = balance [COMMENT START]
End Note
PostgreSQL treats anything after -- as a comment unless it’s wrapped correctly. By transforming negative numbers into parentheses, like (-5) instead of --5, the patch neutralizes the risk of inline comment injection, even when using string substitution inside unsafe queries.
This simple but effective patch showcases how even well-intentioned ORMs can become vulnerable when lower-level SQL syntax rules (like comment delimiters) aren’t handled precisely.
If you enjoyed reading this blog and are passionate about CVE reversing or building hands-on CVE labs for practicing real-world web hacking scenarios, consider contributing to cvePlayground — a collection of intentionally vulnerable applications designed for learning and experimentation.
Check it out on GitHub: https://github.com/awwfensive/cvePlayground