From 4623819f24396eed618750bac4bac1ac81d7e3ba Mon Sep 17 00:00:00 2001 From: AkshayK Date: Thu, 27 Aug 2026 11:42:17 -0400 Subject: [PATCH] cpp: model libpq query functions as SQL-injection sinks Add the PostgreSQL libpq query-execution functions as sql-injection sinks so cpp/sql-injection flags tainted data reaching their SQL text: - PQexec / PQsendQuery: query at argument 1 - PQexecParams / PQsendQueryParams: command at argument 1 - PQprepare / PQsendPrepare: query at argument 2 The *Prepared forms take a prepared-statement name rather than SQL text and are not modeled. Follows the existing Oracle.oci sink convention. --- .../2026-08-27-libpq-sql-sinks.md | 4 ++ cpp/ql/lib/ext/Postgres.model.yml | 16 ++++++++ .../CWE-089/SqlTainted/SqlTainted.expected | 25 ++++++++++++ .../CWE/CWE-089/SqlTainted/test_libpq.c | 38 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 cpp/ql/lib/change-notes/2026-08-27-libpq-sql-sinks.md create mode 100644 cpp/ql/lib/ext/Postgres.model.yml create mode 100644 cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/test_libpq.c diff --git a/cpp/ql/lib/change-notes/2026-08-27-libpq-sql-sinks.md b/cpp/ql/lib/change-notes/2026-08-27-libpq-sql-sinks.md new file mode 100644 index 000000000000..94899fdafafa --- /dev/null +++ b/cpp/ql/lib/change-notes/2026-08-27-libpq-sql-sinks.md @@ -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. diff --git a/cpp/ql/lib/ext/Postgres.model.yml b/cpp/ql/lib/ext/Postgres.model.yml new file mode 100644 index 000000000000..958522e09cbe --- /dev/null +++ b/cpp/ql/lib/ext/Postgres.model.yml @@ -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"] diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/SqlTainted.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/SqlTainted.expected index 2e00cb866749..5dedd13a12bd 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/SqlTainted.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/SqlTainted.expected @@ -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 | @@ -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 | @@ -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 diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/test_libpq.c b/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/test_libpq.c new file mode 100644 index 000000000000..4fc26907b136 --- /dev/null +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-089/SqlTainted/test_libpq.c @@ -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 +}