Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cpp/ql/lib/change-notes/2026-08-27-libpq-sql-sinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added the PostgreSQL libpq query-execution functions (`PQexec`, `PQexecParams`, `PQprepare`, and their asynchronous `PQsendQuery`/`PQsendQueryParams`/`PQsendPrepare` counterparts) as `sql-injection` sinks, so the "Uncontrolled data used in SQL query" query flags tainted data reaching their command/query arguments.
16 changes: 16 additions & 0 deletions cpp/ql/lib/ext/Postgres.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# partial model of the PostgreSQL libpq C API
extensions:
- addsTo:
pack: codeql/cpp-all
extensible: sinkModel
data: # namespace, type, subtypes, name, signature, ext, input, kind, provenance
# Functions that take a raw SQL command string. The `*Params`/`*Prepare` forms still
# interpret their command/query text as SQL, so injection into that text is a sink even
# though the parameter *values* are passed separately. The `*Prepared` forms take a
# prepared-statement name rather than SQL text and are therefore not modeled here.
- ["", "", False, "PQexec", "", "", "Argument[*1]", "sql-injection", "manual"]
- ["", "", False, "PQexecParams", "", "", "Argument[*1]", "sql-injection", "manual"]
- ["", "", False, "PQprepare", "", "", "Argument[*2]", "sql-injection", "manual"]
- ["", "", False, "PQsendQuery", "", "", "Argument[*1]", "sql-injection", "manual"]
- ["", "", False, "PQsendQueryParams", "", "", "Argument[*1]", "sql-injection", "manual"]
- ["", "", False, "PQsendPrepare", "", "", "Argument[*2]", "sql-injection", "manual"]
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
| test.c:106:24:106:29 | query1 | test.c:101:8:101:16 | gets output argument | test.c:106:24:106:29 | query1 | This argument to a SQL query function is derived from $@. | test.c:101:8:101:16 | gets output argument | user input (string read by gets) |
| test.c:107:28:107:33 | query1 | test.c:101:8:101:16 | gets output argument | test.c:107:28:107:33 | query1 | This argument to a SQL query function is derived from $@. | test.c:101:8:101:16 | gets output argument | user input (string read by gets) |
| test.cpp:43:27:43:33 | *access to array | test.cpp:39:27:39:30 | **argv | test.cpp:43:27:43:33 | *access to array | This argument to a SQL query function is derived from $@ and then passed to pqxx::work::exec1((unnamed parameter 0)). | test.cpp:39:27:39:30 | **argv | user input (a command-line argument) |
| test_libpq.c:29:16:29:24 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:29:16:29:24 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
| test_libpq.c:30:22:30:30 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:30:22:30:30 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
| test_libpq.c:31:27:31:35 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:31:27:31:35 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
| test_libpq.c:32:21:32:29 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:32:21:32:29 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
| test_libpq.c:33:27:33:35 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:33:27:33:35 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
| test_libpq.c:34:31:34:39 | userInput | test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:34:31:34:39 | userInput | This argument to a SQL query function is derived from $@. | test_libpq.c:26:8:26:16 | gets output argument | user input (string read by gets) |
edges
| test.c:14:27:14:30 | **argv | test.c:15:20:15:26 | *access to array | provenance | |
| test.c:15:20:15:26 | *access to array | test.c:21:18:21:23 | *query1 | provenance | TaintFunction |
Expand All @@ -20,9 +26,21 @@ edges
| test.c:101:8:101:16 | gets output argument | test.c:106:24:106:29 | query1 | provenance | TaintFunction Sink:MaD:2 |
| test.c:101:8:101:16 | gets output argument | test.c:107:28:107:33 | query1 | provenance | TaintFunction Sink:MaD:1 |
| test.cpp:39:27:39:30 | **argv | test.cpp:43:27:43:33 | *access to array | provenance | |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:29:16:29:24 | userInput | provenance | Sink:MaD:3 |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:30:22:30:30 | userInput | provenance | Sink:MaD:4 |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:31:27:31:35 | userInput | provenance | Sink:MaD:5 |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:32:21:32:29 | userInput | provenance | Sink:MaD:7 |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:33:27:33:35 | userInput | provenance | Sink:MaD:8 |
| test_libpq.c:26:8:26:16 | gets output argument | test_libpq.c:34:31:34:39 | userInput | provenance | Sink:MaD:6 |
models
| 1 | Sink: ; ; false; OCIStmtPrepare2; ; ; Argument[*3]; sql-injection; manual |
| 2 | Sink: ; ; false; OCIStmtPrepare; ; ; Argument[*2]; sql-injection; manual |
| 3 | Sink: ; ; false; PQexec; ; ; Argument[*1]; sql-injection; manual |
| 4 | Sink: ; ; false; PQexecParams; ; ; Argument[*1]; sql-injection; manual |
| 5 | Sink: ; ; false; PQprepare; ; ; Argument[*2]; sql-injection; manual |
| 6 | Sink: ; ; false; PQsendPrepare; ; ; Argument[*2]; sql-injection; manual |
| 7 | Sink: ; ; false; PQsendQuery; ; ; Argument[*1]; sql-injection; manual |
| 8 | Sink: ; ; false; PQsendQueryParams; ; ; Argument[*1]; sql-injection; manual |
nodes
| test.c:14:27:14:30 | **argv | semmle.label | **argv |
| test.c:15:20:15:26 | *access to array | semmle.label | *access to array |
Expand All @@ -41,4 +59,11 @@ nodes
| test.c:107:28:107:33 | query1 | semmle.label | query1 |
| test.cpp:39:27:39:30 | **argv | semmle.label | **argv |
| test.cpp:43:27:43:33 | *access to array | semmle.label | *access to array |
| test_libpq.c:26:8:26:16 | gets output argument | semmle.label | gets output argument |
| test_libpq.c:29:16:29:24 | userInput | semmle.label | userInput |
| test_libpq.c:30:22:30:30 | userInput | semmle.label | userInput |
| test_libpq.c:31:27:31:35 | userInput | semmle.label | userInput |
| test_libpq.c:32:21:32:29 | userInput | semmle.label | userInput |
| test_libpq.c:33:27:33:35 | userInput | semmle.label | userInput |
| test_libpq.c:34:31:34:39 | userInput | semmle.label | userInput |
subpaths
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Test case for the PostgreSQL libpq SQL-injection sinks modeled in Postgres.model.yml.
// Associated with CWE-089: SQL injection.

typedef unsigned long size_t;
typedef unsigned int Oid;
typedef struct pg_conn PGconn;
typedef struct pg_result PGresult;

PGresult *PQexec(PGconn *conn, const char *query);
PGresult *PQexecParams(PGconn *conn, const char *command, int nParams,
const Oid *paramTypes, const char *const *paramValues,
const int *paramLengths, const int *paramFormats, int resultFormat);
PGresult *PQprepare(PGconn *conn, const char *stmtName, const char *query, int nParams,
const Oid *paramTypes);
int PQsendQuery(PGconn *conn, const char *query);
int PQsendQueryParams(PGconn *conn, const char *command, int nParams, const Oid *paramTypes,
const char *const *paramValues, const int *paramLengths,
const int *paramFormats, int resultFormat);
int PQsendPrepare(PGconn *conn, const char *stmtName, const char *query, int nParams,
const Oid *paramTypes);

char *gets(char *s);

void libpqTests(PGconn *conn) {
char userInput[1000];
gets(userInput); // $ Source

// A user-controlled string is interpreted as SQL.
PQexec(conn, userInput); // $ Alert
PQexecParams(conn, userInput, 0, 0, 0, 0, 0, 0); // $ Alert
PQprepare(conn, "stmt", userInput, 0, 0); // $ Alert
PQsendQuery(conn, userInput); // $ Alert
PQsendQueryParams(conn, userInput, 0, 0, 0, 0, 0, 0); // $ Alert
PQsendPrepare(conn, "stmt", userInput, 0, 0); // $ Alert

// A constant query is safe.
PQexec(conn, "SELECT 1"); // GOOD
}